From 5be023a6c6bede073f8eb3ea8ea4771acf217e7b Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 11 Sep 2026 14:52:26 +0000 Subject: [PATCH 1/3] fix(parser): dedupe Codex inferences with empty IDs From 5657df9882509a10ab5ac62466b8fa0562d5477e Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 11 Sep 2026 14:52:27 +0000 Subject: [PATCH 2/3] test(parser): Codex empty-ID inference dedup --- internal/parser/codex_dedup_test.go | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 internal/parser/codex_dedup_test.go diff --git a/internal/parser/codex_dedup_test.go b/internal/parser/codex_dedup_test.go new file mode 100644 index 0000000..a5b3204 --- /dev/null +++ b/internal/parser/codex_dedup_test.go @@ -0,0 +1,83 @@ +package parser + +import ( + "encoding/json" + "testing" +) + +func TestBuildTraceFromCodex_DedupsEmptyInferenceIDs(t *testing.T) { + inf, err := json.Marshal(codexInference{ + Model: "gpt-5", + InputTokens: 100, + OutputTokens: 50, + }) + if err != nil { + t.Fatal(err) + } + + entries := []codexEntry{ + { + Type: "tool_call_started", + ID: "tool-1", + SessionID: "s1", + Timestamp: "2026-07-18T10:00:00.000Z", + Data: json.RawMessage(`{"name":"Edit","input":"{\"file_path\":\"a.go\"}"}`), + }, + { + Type: "inference_completed", + ID: "", + SessionID: "s1", + Timestamp: "2026-07-18T10:00:01.000Z", + Data: inf, + }, + { + // Duplicate empty-ID inference with identical payload/timestamp. + Type: "inference_completed", + ID: "", + SessionID: "s1", + Timestamp: "2026-07-18T10:00:01.000Z", + Data: inf, + }, + } + + tr, err := buildTraceFromCodex(entries, "synthetic.codex") + if err != nil { + t.Fatalf("buildTraceFromCodex: %v", err) + } + it := tr.Get(0) + if it == nil { + t.Fatal("expected iteration") + } + if it.Tokens.InputTokens != 100 { + t.Fatalf("InputTokens = %d, want 100 (deduped once)", it.Tokens.InputTokens) + } + if it.Tokens.OutputTokens != 50 { + t.Fatalf("OutputTokens = %d, want 50 (deduped once)", it.Tokens.OutputTokens) + } +} + +func TestBuildTraceFromCodex_CountsDistinctEmptyIDInferences(t *testing.T) { + inf1, _ := json.Marshal(codexInference{Model: "gpt-5", InputTokens: 10, OutputTokens: 5}) + inf2, _ := json.Marshal(codexInference{Model: "gpt-5", InputTokens: 20, OutputTokens: 7}) + entries := []codexEntry{ + { + Type: "tool_call_started", + SessionID: "s1", + Timestamp: "2026-07-18T10:00:00.000Z", + Data: json.RawMessage(`{"name":"Edit","input":"{\"file_path\":\"a.go\"}"}`), + }, + {Type: "inference_completed", Timestamp: "2026-07-18T10:00:01.000Z", Data: inf1}, + {Type: "inference_completed", Timestamp: "2026-07-18T10:00:02.000Z", Data: inf2}, + } + tr, err := buildTraceFromCodex(entries, "synthetic.codex") + if err != nil { + t.Fatal(err) + } + it := tr.Get(0) + if it.Tokens.InputTokens != 30 { + t.Fatalf("InputTokens = %d, want 30", it.Tokens.InputTokens) + } + if it.Tokens.OutputTokens != 12 { + t.Fatalf("OutputTokens = %d, want 12", it.Tokens.OutputTokens) + } +} From 51532ac2d5cc71368f9f982a0e936e7fbb972295 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 11 Sep 2026 14:52:54 +0000 Subject: [PATCH 3/3] fix(parser): dedupe Codex inferences with empty IDs --- internal/parser/codex.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/parser/codex.go b/internal/parser/codex.go index c2300fc..8e3e9db 100644 --- a/internal/parser/codex.go +++ b/internal/parser/codex.go @@ -141,12 +141,16 @@ func buildTraceFromCodex(entries []codexEntry, sourcePath string) (*trace.Trace, model = inf.Model } - if entry.ID != "" && seenInference[entry.ID] { - continue + dedupKey := entry.ID + if dedupKey == "" { + // Empty IDs used to bypass dedup and double-count tokens/cost. + dedupKey = fmt.Sprintf("%s|%d|%d|%d|%s", + inf.Model, inf.InputTokens, inf.OutputTokens, inf.ReasoningOut, entry.Timestamp) } - if entry.ID != "" { - seenInference[entry.ID] = true + if seenInference[dedupKey] { + continue } + seenInference[dedupKey] = true if currentIter != nil { currentIter.Tokens.InputTokens += inf.InputTokens