History

fix(server): usage no longer double-counts forked Codex sessions (#5887)

Theo Browne committed 3 months ago

Commit 0d388661 parent f993fa13 files · +159 −3Browse files

Showing the changes to apps/server/src/usage/usageTranscripts.test.ts (1 of 3 files). Show all

Unified · Split

apps/server/src/usage/usageTranscripts.test.ts+100−0
@@ -134,6 +134,106 @@ describe("parseCodexLine", () => { · 133 unmodified lines
134134 parseCodexLine(turnContext, state);
135135 expect(parseCodexLine(tokenCount(100, 0, 10, 0), state)).not.toBeNull();
136136 });
137+
138+ // A forked/subagent rollout opens with the parent's history copied in and
139+ // every line re-stamped to the fork instant, then the ancestors' session
140+ // metas. Counting those again multiplied usage ~1.85x on real data (#5758).
141+ describe("forked rollouts", () => {
142+ const meta = (overrides: {
143+ id: string;
144+ timestamp: string;
145+ forkedFromId?: string;
146+ spawnParentId?: string;
147+ }) =>
148+ JSON.stringify({
149+ type: "session_meta",
150+ timestamp: overrides.timestamp,
151+ payload: {
152+ type: "session_meta",
153+ id: overrides.id,
154+ ...(overrides.forkedFromId === undefined
155+ ? {}
156+ : { forked_from_id: overrides.forkedFromId }),
157+ ...(overrides.spawnParentId === undefined
158+ ? {}
159+ : {
160+ source: {
161+ subagent: { thread_spawn: { parent_thread_id: overrides.spawnParentId } },
162+ },
163+ }),
164+ },
165+ });
166+ const stamped = (timestamp: string, line: string) => {
167+ const parsed = JSON.parse(line) as { timestamp: string };
168+ parsed.timestamp = timestamp;
169+ return JSON.stringify(parsed);
170+ };
171+
172+ it("keeps the child session id over copied ancestor metas", () => {
173+ const state = initialCodexScanState();
174+ parseCodexLine(meta({ id: "child", timestamp: "2026-08-01T05:00:00.000Z" }), state);
175+ parseCodexLine(meta({ id: "parent", timestamp: "2026-08-01T05:00:00.000Z" }), state);
176+ parseCodexLine(turnContext, state);
177+ const record = parseCodexLine(tokenCount(100, 0, 10, 0), state);
178+
179+ expect(record?.sessionId).toBe("child");
180+ });
181+
182+ it("drops the re-stamped copied burst and keeps the first real event", () => {
183+ const state = initialCodexScanState();
184+ const forkInstant = "2026-08-01T05:00:00.000Z";
185+ parseCodexLine(meta({ id: "child", timestamp: forkInstant, forkedFromId: "parent" }), state);
186+ parseCodexLine(meta({ id: "parent", timestamp: forkInstant }), state);
187+ parseCodexLine(stamped(forkInstant, turnContext), state);
188+
189+ // Copied history: written in one burst at the fork instant.
190+ expect(
191+ parseCodexLine(stamped("2026-08-01T05:00:00.001Z", tokenCount(100, 0, 10, 0)), state),
192+ ).toBeNull();
193+ expect(
194+ parseCodexLine(stamped("2026-08-01T05:00:00.002Z", tokenCount(200, 0, 20, 0)), state),
195+ ).toBeNull();
196+
197+ // The child's first genuine turn lands seconds later and must count.
198+ const real = parseCodexLine(
199+ stamped("2026-08-01T05:00:06.000Z", tokenCount(300, 0, 30, 0)),
200+ state,
201+ );
202+ expect(real).not.toBeNull();
203+ expect(real?.totals.outputTokens).toBe(30);
204+
205+ // Suppression never restarts, even for closely spaced later events.
206+ const next = parseCodexLine(
207+ stamped("2026-08-01T05:00:06.100Z", tokenCount(400, 0, 40, 0)),
208+ state,
209+ );
210+ expect(next).not.toBeNull();
211+ });
212+
213+ it("recognizes subagent spawns without forked_from_id", () => {
214+ const state = initialCodexScanState();
215+ const spawnInstant = "2026-08-01T05:00:00.000Z";
216+ parseCodexLine(
217+ meta({ id: "child", timestamp: spawnInstant, spawnParentId: "parent" }),
218+ state,
219+ );
220+ parseCodexLine(stamped(spawnInstant, turnContext), state);
221+ expect(
222+ parseCodexLine(stamped("2026-08-01T05:00:00.001Z", tokenCount(100, 0, 10, 0)), state),
223+ ).toBeNull();
224+ });
225+
226+ it("does not suppress anything in a rollout that is not a fork", () => {
227+ const state = initialCodexScanState();
228+ parseCodexLine(meta({ id: "root", timestamp: "2026-08-01T05:00:00.000Z" }), state);
229+ parseCodexLine(stamped("2026-08-01T05:00:00.100Z", turnContext), state);
230+ const record = parseCodexLine(
231+ stamped("2026-08-01T05:00:00.200Z", tokenCount(100, 0, 10, 0)),
232+ state,
233+ );
234+ expect(record).not.toBeNull();
235+ });
236+ });
137237 });
138238
139239 describe("totalTokens", () => {