Skip to content

Commit 73f174a

Browse files
committed
fix(projects-overlay): count rounds across resume windows
1 parent 4216d76 commit 73f174a

2 files changed

Lines changed: 198 additions & 56 deletions

File tree

cli/src/workflow/projects_overlay_index.rs

Lines changed: 194 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ fn project_entry_for_progress_file(
112112
index.project_started.user_message.as_deref(),
113113
)
114114
.unwrap_or_default();
115-
let rounds = project_list_rounds(&status, &index);
115+
let rounds = project_list_rounds(&index);
116116
let started_at_unix_secs = project_started_at_unix_secs(workdir, &index);
117117

118118
Ok(DiscoveredOverlayProject {
@@ -168,27 +168,28 @@ fn project_list_status(index: &PotterRolloutResumeIndex) -> PotterProjectListSta
168168
}
169169
}
170170

171-
fn project_list_rounds(status: &PotterProjectListStatus, index: &PotterRolloutResumeIndex) -> u32 {
172-
if *status == PotterProjectListStatus::Succeeded
173-
&& let Some(rounds) = index.completed_rounds.iter().rev().find_map(|round| {
174-
round
175-
.project_succeeded
176-
.as_ref()
177-
.map(|succeeded| succeeded.rounds)
178-
})
179-
{
180-
return rounds;
181-
}
171+
fn project_list_rounds(index: &PotterRolloutResumeIndex) -> u32 {
172+
// Count total rounds across the entire project lifecycle (including prior `resume` windows).
173+
//
174+
// Older/partially-corrupted logs might contain only the latest round marker. Preserve the
175+
// previous best-effort behavior by ensuring we never report fewer rounds than the last
176+
// recorded round index.
177+
let completed_rounds: u32 = index.completed_rounds.len().try_into().unwrap_or(u32::MAX);
178+
let counted = completed_rounds.saturating_add(u32::from(index.unfinished_round.is_some()));
182179

183-
if let Some(unfinished) = index.unfinished_round.as_ref() {
184-
return unfinished.round_current;
185-
}
186-
187-
index
188-
.completed_rounds
189-
.last()
180+
let last_round_current = index
181+
.unfinished_round
182+
.as_ref()
190183
.map(|round| round.round_current)
191-
.unwrap_or_default()
184+
.or_else(|| {
185+
index
186+
.completed_rounds
187+
.last()
188+
.map(|round| round.round_current)
189+
})
190+
.unwrap_or_default();
191+
192+
counted.max(last_round_current)
192193
}
193194

194195
fn project_started_at_unix_secs(workdir: &Path, index: &PotterRolloutResumeIndex) -> Option<u64> {
@@ -367,7 +368,7 @@ original goal line
367368
fn write_potter_rollout(
368369
project_dir: &Path,
369370
user_prompt_file: &Path,
370-
round_current: u32,
371+
rounds: u32,
371372
round_total: u32,
372373
rollout_path: &Path,
373374
outcome: PotterRoundOutcome,
@@ -388,46 +389,55 @@ original goal line
388389
)
389390
.expect("append project_started");
390391

391-
crate::workflow::rollout::append_line(
392-
&potter_rollout_path,
393-
&crate::workflow::rollout::PotterRolloutLine::RoundStarted {
394-
current: round_current,
395-
total: round_total,
396-
},
397-
)
398-
.expect("append round_started");
399-
400-
crate::workflow::rollout::append_line(
401-
&potter_rollout_path,
402-
&crate::workflow::rollout::PotterRolloutLine::RoundConfigured {
403-
thread_id,
404-
rollout_path: rollout_path.to_path_buf(),
405-
service_tier: None,
406-
rollout_path_raw: None,
407-
rollout_base_dir: None,
408-
},
409-
)
410-
.expect("append round_configured");
392+
for round_current in 1..=rounds {
393+
crate::workflow::rollout::append_line(
394+
&potter_rollout_path,
395+
&crate::workflow::rollout::PotterRolloutLine::RoundStarted {
396+
current: round_current,
397+
total: round_total,
398+
},
399+
)
400+
.expect("append round_started");
411401

412-
if let Some(rounds) = succeeded_rounds {
413402
crate::workflow::rollout::append_line(
414403
&potter_rollout_path,
415-
&crate::workflow::rollout::PotterRolloutLine::ProjectSucceeded {
416-
rounds,
417-
duration_secs: 1,
418-
user_prompt_file: user_prompt_file.to_path_buf(),
419-
git_commit_start: "".to_string(),
420-
git_commit_end: "".to_string(),
404+
&crate::workflow::rollout::PotterRolloutLine::RoundConfigured {
405+
thread_id,
406+
rollout_path: rollout_path.to_path_buf(),
407+
service_tier: None,
408+
rollout_path_raw: None,
409+
rollout_base_dir: None,
421410
},
422411
)
423-
.expect("append project_succeeded");
424-
}
412+
.expect("append round_configured");
413+
414+
if round_current == rounds
415+
&& let Some(project_succeeded_rounds) = succeeded_rounds
416+
{
417+
crate::workflow::rollout::append_line(
418+
&potter_rollout_path,
419+
&crate::workflow::rollout::PotterRolloutLine::ProjectSucceeded {
420+
rounds: project_succeeded_rounds,
421+
duration_secs: 1,
422+
user_prompt_file: user_prompt_file.to_path_buf(),
423+
git_commit_start: "".to_string(),
424+
git_commit_end: "".to_string(),
425+
},
426+
)
427+
.expect("append project_succeeded");
428+
}
425429

426-
crate::workflow::rollout::append_line(
427-
&potter_rollout_path,
428-
&crate::workflow::rollout::PotterRolloutLine::RoundFinished { outcome },
429-
)
430-
.expect("append round_finished");
430+
let outcome = if round_current == rounds {
431+
outcome.clone()
432+
} else {
433+
PotterRoundOutcome::Completed
434+
};
435+
crate::workflow::rollout::append_line(
436+
&potter_rollout_path,
437+
&crate::workflow::rollout::PotterRolloutLine::RoundFinished { outcome },
438+
)
439+
.expect("append round_finished");
440+
}
431441
}
432442

433443
#[test]
@@ -566,6 +576,34 @@ original goal line
566576
},
567577
)
568578
.expect("append project_started");
579+
580+
crate::workflow::rollout::append_line(
581+
&potter_rollout_path,
582+
&crate::workflow::rollout::PotterRolloutLine::RoundStarted {
583+
current: 1,
584+
total: 10,
585+
},
586+
)
587+
.expect("append round_started");
588+
crate::workflow::rollout::append_line(
589+
&potter_rollout_path,
590+
&crate::workflow::rollout::PotterRolloutLine::RoundConfigured {
591+
thread_id,
592+
rollout_path: Path::new("live.jsonl").to_path_buf(),
593+
service_tier: None,
594+
rollout_path_raw: None,
595+
rollout_base_dir: None,
596+
},
597+
)
598+
.expect("append round_configured");
599+
crate::workflow::rollout::append_line(
600+
&potter_rollout_path,
601+
&crate::workflow::rollout::PotterRolloutLine::RoundFinished {
602+
outcome: PotterRoundOutcome::Completed,
603+
},
604+
)
605+
.expect("append round_finished");
606+
569607
crate::workflow::rollout::append_line(
570608
&potter_rollout_path,
571609
&crate::workflow::rollout::PotterRolloutLine::RoundStarted {
@@ -592,6 +630,107 @@ original goal line
592630
assert_eq!(rows[0].rounds, 2);
593631
}
594632

633+
#[test]
634+
fn discover_projects_rounds_count_across_resume_windows() {
635+
let temp = tempfile::tempdir().expect("tempdir");
636+
let workdir = temp.path();
637+
638+
let main = write_main(workdir, ".codexpotter/projects/2026/03/04/1", None);
639+
let rollout = workdir.join("rollout.jsonl");
640+
write_rollout_with_timestamp(&rollout, "2026-03-04T00:00:00.000Z");
641+
642+
let project_dir = main.parent().expect("project dir");
643+
let user_prompt_file = main.strip_prefix(workdir).expect("rel");
644+
645+
// First iteration window: 3 rounds, then stop.
646+
write_potter_rollout(
647+
project_dir,
648+
user_prompt_file,
649+
3,
650+
10,
651+
Path::new("rollout.jsonl"),
652+
PotterRoundOutcome::Interrupted,
653+
None,
654+
);
655+
656+
// Resumed window: rounds reset to 1, succeeds in 2 rounds.
657+
let potter_rollout_path =
658+
project_dir.join(crate::workflow::rollout::POTTER_ROLLOUT_FILENAME);
659+
let thread_id =
660+
codex_protocol::ThreadId::from_string("019ca423-63d9-7641-ae83-db060ad3c000")
661+
.expect("thread id");
662+
663+
crate::workflow::rollout::append_line(
664+
&potter_rollout_path,
665+
&crate::workflow::rollout::PotterRolloutLine::RoundStarted {
666+
current: 1,
667+
total: 10,
668+
},
669+
)
670+
.expect("append round_started");
671+
crate::workflow::rollout::append_line(
672+
&potter_rollout_path,
673+
&crate::workflow::rollout::PotterRolloutLine::RoundConfigured {
674+
thread_id,
675+
rollout_path: PathBuf::from("rollout.jsonl"),
676+
service_tier: None,
677+
rollout_path_raw: None,
678+
rollout_base_dir: None,
679+
},
680+
)
681+
.expect("append round_configured");
682+
crate::workflow::rollout::append_line(
683+
&potter_rollout_path,
684+
&crate::workflow::rollout::PotterRolloutLine::RoundFinished {
685+
outcome: PotterRoundOutcome::Completed,
686+
},
687+
)
688+
.expect("append round_finished");
689+
690+
crate::workflow::rollout::append_line(
691+
&potter_rollout_path,
692+
&crate::workflow::rollout::PotterRolloutLine::RoundStarted {
693+
current: 2,
694+
total: 10,
695+
},
696+
)
697+
.expect("append round_started");
698+
crate::workflow::rollout::append_line(
699+
&potter_rollout_path,
700+
&crate::workflow::rollout::PotterRolloutLine::RoundConfigured {
701+
thread_id,
702+
rollout_path: PathBuf::from("rollout.jsonl"),
703+
service_tier: None,
704+
rollout_path_raw: None,
705+
rollout_base_dir: None,
706+
},
707+
)
708+
.expect("append round_configured");
709+
crate::workflow::rollout::append_line(
710+
&potter_rollout_path,
711+
&crate::workflow::rollout::PotterRolloutLine::ProjectSucceeded {
712+
rounds: 2,
713+
duration_secs: 1,
714+
user_prompt_file: user_prompt_file.to_path_buf(),
715+
git_commit_start: "".to_string(),
716+
git_commit_end: "".to_string(),
717+
},
718+
)
719+
.expect("append project_succeeded");
720+
crate::workflow::rollout::append_line(
721+
&potter_rollout_path,
722+
&crate::workflow::rollout::PotterRolloutLine::RoundFinished {
723+
outcome: PotterRoundOutcome::Completed,
724+
},
725+
)
726+
.expect("append round_finished");
727+
728+
let rows = discover_projects_for_overlay(workdir).expect("discover");
729+
assert_eq!(rows.len(), 1);
730+
assert_eq!(rows[0].status, PotterProjectListStatus::Succeeded);
731+
assert_eq!(rows[0].rounds, 5);
732+
}
733+
595734
#[test]
596735
fn discover_resumable_projects_filters_missing_rollout_files() {
597736
let temp = tempfile::tempdir().expect("tempdir");

protocol/src/protocol.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,10 @@ pub struct PotterProjectListEntry {
358358
pub description: String,
359359
/// Project start time derived from upstream rollouts, when available.
360360
pub started_at_unix_secs: Option<u64>,
361-
/// Project rounds count rendered in the left list (best-effort).
361+
/// Total rounds observed for the project (best-effort).
362+
///
363+
/// This value is derived from the append-only `potter-rollout.jsonl` log and is intended to
364+
/// remain stable across `codex-potter resume` windows.
362365
pub rounds: u32,
363366
pub status: PotterProjectListStatus,
364367
}

0 commit comments

Comments
 (0)