Skip to content

Commit a668d2c

Browse files
committed
fix(codex): preserve replay cumulative baseline
When thread_spawn replay entries are skipped, keep their cumulative total_token_usage as the parser baseline. Without that baseline, Codex logs that only provide total_token_usage on the first real subagent entry are counted as the full replayed cumulative total instead of the post-replay delta. Add a regression fixture that skips two replayed cumulative entries and verifies the following real entry is reported as a 100 input-token delta rather than the 1600-token cumulative total.
1 parent a614b36 commit a668d2c

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

‎rust/crates/ccusage/src/adapter/codex/loader.rs‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,87 @@ mod tests {
660660
}
661661
}
662662

663+
#[test]
664+
fn keeps_cumulative_baseline_when_skipping_subagent_replay() {
665+
let fixture = fs_fixture!({
666+
"2026-05-12T08-03-00-subagent.jsonl": [
667+
json!({
668+
"timestamp": "2026-05-12T08:03:00.000Z",
669+
"type": "session_meta",
670+
"payload": {
671+
"id": "subagent-abc",
672+
"source": {
673+
"subagent": {
674+
"thread_spawn": {
675+
"parent_thread_id": "parent-xyz"
676+
}
677+
}
678+
}
679+
},
680+
})
681+
.to_string(),
682+
json!({
683+
"timestamp": "2026-05-12T08:03:00.000Z",
684+
"type": "event_msg",
685+
"payload": {
686+
"type": "token_count",
687+
"info": {
688+
"total_token_usage": {
689+
"input_tokens": 1_000,
690+
"cached_input_tokens": 100,
691+
"output_tokens": 200,
692+
"total_tokens": 1_200,
693+
},
694+
},
695+
},
696+
})
697+
.to_string(),
698+
json!({
699+
"timestamp": "2026-05-12T08:03:00.000Z",
700+
"type": "event_msg",
701+
"payload": {
702+
"type": "token_count",
703+
"info": {
704+
"total_token_usage": {
705+
"input_tokens": 1_500,
706+
"cached_input_tokens": 150,
707+
"output_tokens": 300,
708+
"total_tokens": 1_800,
709+
},
710+
},
711+
},
712+
})
713+
.to_string(),
714+
json!({
715+
"timestamp": "2026-05-12T08:04:00.000Z",
716+
"type": "event_msg",
717+
"payload": {
718+
"type": "token_count",
719+
"info": {
720+
"total_token_usage": {
721+
"input_tokens": 1_600,
722+
"cached_input_tokens": 160,
723+
"output_tokens": 320,
724+
"total_tokens": 1_920,
725+
},
726+
"model": "gpt-5.2",
727+
},
728+
},
729+
})
730+
.to_string(),
731+
]
732+
.join("\n"),
733+
});
734+
735+
let events = load_codex_events_from_directory(fixture.root(), true).unwrap();
736+
737+
assert_eq!(events.len(), 1);
738+
assert_eq!(events[0].input_tokens, 100);
739+
assert_eq!(events[0].cached_input_tokens, 10);
740+
assert_eq!(events[0].output_tokens, 20);
741+
assert_eq!(events[0].total_tokens, 120);
742+
}
743+
663744
#[test]
664745
fn skips_replayed_history_across_multiple_subagent_files() {
665746
let parent_line = json!({

‎rust/crates/ccusage/src/adapter/codex/parser.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,15 @@ pub(super) fn visit_codex_session_file(
164164
.get(0..19)
165165
.is_some_and(|sec| sec.len() == 19 && sec == replay_ts.as_slice());
166166
if matches_replay {
167+
if let Some(total_usage) = value
168+
.payload
169+
.as_ref()
170+
.and_then(|payload| payload.info.as_ref())
171+
.and_then(|info| info.total_token_usage.as_ref())
172+
.copied()
173+
{
174+
previous_totals.replace(total_usage);
175+
}
167176
continue;
168177
}
169178
skip_replay = false;

0 commit comments

Comments
 (0)