@@ -122,6 +122,32 @@ function convertToEventUsage(
122122 } ;
123123}
124124
125+ function createTokenUsageEventKey ( event : TokenUsageEvent ) : string {
126+ return JSON . stringify ( [
127+ event . timestamp ,
128+ event . model ?? '' ,
129+ event . inputTokens ,
130+ event . cachedInputTokens ,
131+ event . outputTokens ,
132+ event . reasoningOutputTokens ,
133+ event . totalTokens ,
134+ ] ) ;
135+ }
136+
137+ function deduplicateTokenUsageEvents ( events : TokenUsageEvent [ ] ) : TokenUsageEvent [ ] {
138+ const seen = new Set < string > ( ) ;
139+ const deduplicated : TokenUsageEvent [ ] = [ ] ;
140+ for ( const event of events ) {
141+ const key = createTokenUsageEventKey ( event ) ;
142+ if ( seen . has ( key ) ) {
143+ continue ;
144+ }
145+ seen . add ( key ) ;
146+ deduplicated . push ( event ) ;
147+ }
148+ return deduplicated ;
149+ }
150+
125151function asRecord ( value : unknown ) : Record < string , unknown > | null {
126152 return value != null && typeof value === 'object' && ! Array . isArray ( value )
127153 ? ( value as Record < string , unknown > )
@@ -411,7 +437,9 @@ export async function loadTokenUsageEvents(): Promise<TokenUsageEvent[]> {
411437 const directoryEvents = await Promise . all (
412438 getCodexSessionsPaths ( ) . map ( loadTokenUsageEventsFromDirectory ) ,
413439 ) ;
414- return directoryEvents . flat ( ) . sort ( ( a , b ) => compareStrings ( a . timestamp , b . timestamp ) ) ;
440+ return deduplicateTokenUsageEvents ( directoryEvents . flat ( ) ) . sort ( ( a , b ) =>
441+ compareStrings ( a . timestamp , b . timestamp ) ,
442+ ) ;
415443}
416444
417445async function runCodexWorker ( data : CodexWorkerData ) : Promise < void > {
@@ -734,6 +762,78 @@ if (import.meta.vitest != null) {
734762 { sessionId : 'b' , model : 'gpt-5.2' , inputTokens : 20 } ,
735763 ] ) ;
736764 } ) ;
765+
766+ it ( 'deduplicates copied branch history across session files' , async ( ) => {
767+ const copiedHistory = [
768+ JSON . stringify ( {
769+ timestamp : '2026-05-12T08:00:00.000Z' ,
770+ type : 'turn_context' ,
771+ payload : {
772+ model : 'gpt-5.2' ,
773+ } ,
774+ } ) ,
775+ JSON . stringify ( {
776+ timestamp : '2026-05-12T08:01:00.000Z' ,
777+ type : 'event_msg' ,
778+ payload : {
779+ type : 'token_count' ,
780+ info : {
781+ total_token_usage : {
782+ input_tokens : 1_000 ,
783+ cached_input_tokens : 100 ,
784+ output_tokens : 200 ,
785+ reasoning_output_tokens : 20 ,
786+ total_tokens : 1_200 ,
787+ } ,
788+ } ,
789+ } ,
790+ } ) ,
791+ ] . join ( '\n' ) ;
792+
793+ await using fixture = await createFixture ( {
794+ sessions : {
795+ 'project-parent.jsonl' : copiedHistory ,
796+ 'project-branch.jsonl' : [
797+ copiedHistory ,
798+ JSON . stringify ( {
799+ timestamp : '2026-05-12T08:02:00.000Z' ,
800+ type : 'event_msg' ,
801+ payload : {
802+ type : 'token_count' ,
803+ info : {
804+ total_token_usage : {
805+ input_tokens : 1_600 ,
806+ cached_input_tokens : 300 ,
807+ output_tokens : 450 ,
808+ reasoning_output_tokens : 40 ,
809+ total_tokens : 2_050 ,
810+ } ,
811+ } ,
812+ } ,
813+ } ) ,
814+ ] . join ( '\n' ) ,
815+ } ,
816+ } ) ;
817+ vi . stubEnv ( 'CODEX_HOME' , fixture . path ) ;
818+
819+ const events = await loadTokenUsageEvents ( ) ;
820+ expect ( events ) . toMatchObject ( [
821+ {
822+ inputTokens : 1_000 ,
823+ cachedInputTokens : 100 ,
824+ outputTokens : 200 ,
825+ totalTokens : 1_200 ,
826+ } ,
827+ {
828+ sessionId : 'project-branch' ,
829+ inputTokens : 600 ,
830+ cachedInputTokens : 200 ,
831+ outputTokens : 250 ,
832+ totalTokens : 850 ,
833+ } ,
834+ ] ) ;
835+ expect ( events ) . toHaveLength ( 2 ) ;
836+ } ) ;
737837 } ) ;
738838
739839 describe ( 'getCodexWorkerThreadCount' , ( ) => {
0 commit comments