@@ -121,6 +121,30 @@ function parseApiCall(entry: JournalEntry): ParsedApiCall | null {
121121 }
122122}
123123
124+ function dedupeStreamingMessageIds ( entries : JournalEntry [ ] ) : JournalEntry [ ] {
125+ const firstIdxById = new Map < string , number > ( )
126+ const lastIdxById = new Map < string , number > ( )
127+ for ( let i = 0 ; i < entries . length ; i ++ ) {
128+ const id = getMessageId ( entries [ i ] ! )
129+ if ( ! id ) continue
130+ if ( ! firstIdxById . has ( id ) ) firstIdxById . set ( id , i )
131+ lastIdxById . set ( id , i )
132+ }
133+ if ( lastIdxById . size === 0 ) return entries
134+ const result : JournalEntry [ ] = [ ]
135+ for ( let i = 0 ; i < entries . length ; i ++ ) {
136+ const id = getMessageId ( entries [ i ] ! )
137+ if ( id && lastIdxById . get ( id ) !== i ) continue
138+ if ( id && firstIdxById . get ( id ) !== i ) {
139+ const firstTs = entries [ firstIdxById . get ( id ) ! ] ! . timestamp
140+ result . push ( { ...entries [ i ] ! , timestamp : firstTs ?? entries [ i ] ! . timestamp } )
141+ continue
142+ }
143+ result . push ( entries [ i ] ! )
144+ }
145+ return result
146+ }
147+
124148function groupIntoTurns ( entries : JournalEntry [ ] , seenMsgIds : Set < string > ) : ParsedTurn [ ] {
125149 const turns : ParsedTurn [ ] = [ ]
126150 let currentUserMessage = ''
@@ -291,7 +315,8 @@ async function parseSessionFile(
291315 if ( entries . length === 0 ) return null
292316
293317 const sessionId = basename ( filePath , '.jsonl' )
294- let turns = groupIntoTurns ( entries , seenMsgIds )
318+ const dedupedEntries = dedupeStreamingMessageIds ( entries )
319+ let turns = groupIntoTurns ( dedupedEntries , seenMsgIds )
295320 if ( dateRange ) {
296321 // Bucket a turn by the timestamp of its first assistant call (when the cost was
297322 // actually incurred). Filtering entries directly produced orphan assistant calls
0 commit comments