mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(codex-usage): count fork-copied Codex token events once (#33478)
Codex usage no longer double-counts forked sessions: forks copy the parent's token_count events with fresh timestamps, so cumulative events are now keyed on turn id + totals (timestamp fallback when no turn id); the turn id is kept in the incremental-scan checkpoint and the usage cache is rebuilt once. Fixes #19139 Supersedes #19152 Co-authored-by: Jio Kim <26668612+MeroZemory@users.noreply.github.com>
This commit is contained in:
@@ -76,6 +76,7 @@ function createParseContext(
|
||||
sessionCwd: resume.sessionCwd,
|
||||
currentCwd: resume.currentCwd,
|
||||
currentModel: resume.currentModel,
|
||||
currentTurnId: resume.currentTurnId,
|
||||
previousTotals: resume.previousTotals,
|
||||
totalOnlyBaselinePending: false
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export async function buildCodexRolloutResumeState(
|
||||
sessionCwd: context.sessionCwd,
|
||||
currentCwd: context.currentCwd,
|
||||
currentModel: context.currentModel,
|
||||
currentTurnId: context.currentTurnId,
|
||||
previousTotals: context.previousTotals
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import type { UsageProvider } from '../usage/usage-provider-contract'
|
||||
import { scanCodexUsageFilesViaWorker } from '../usage/usage-scan-worker-spawn'
|
||||
import type { CodexUsageDailyAggregate, CodexUsageSession } from './types'
|
||||
|
||||
// Rescan older caches: v5 deduplicates forks across sessions; v6 adds per-request long-context counts.
|
||||
export const CODEX_USAGE_SCHEMA_VERSION = 6
|
||||
// Rescan older caches: v5 deduplicates forks across sessions; v6 adds per-request long-context counts;
|
||||
// v7 keys fork copies on turn id because forks rewrite copied timestamps.
|
||||
export const CODEX_USAGE_SCHEMA_VERSION = 7
|
||||
|
||||
export const codexUsageProvider = {
|
||||
id: 'codex',
|
||||
|
||||
@@ -19,6 +19,7 @@ export type CodexUsageParseContext = {
|
||||
sessionCwd: string | null
|
||||
currentCwd: string | null
|
||||
currentModel: string | null
|
||||
currentTurnId?: string | null
|
||||
previousTotals: CodexUsageRawUsage | null
|
||||
totalOnlyBaselinePending?: boolean
|
||||
}
|
||||
@@ -86,6 +87,7 @@ export function parseCodexUsageRecord(
|
||||
}
|
||||
|
||||
if (parsed.type === 'turn_context') {
|
||||
context.currentTurnId = extractString(parsed.payload.turn_id)
|
||||
context.currentCwd =
|
||||
extractString(parsed.payload.cwd) ?? context.currentCwd ?? context.sessionCwd
|
||||
context.currentModel = extractModel(parsed.payload) ?? context.currentModel
|
||||
@@ -149,7 +151,12 @@ export function parseCodexUsageRecord(
|
||||
return {
|
||||
sessionId: context.sessionId,
|
||||
timestamp: parsed.timestamp,
|
||||
eventKey: buildCodexUsageEventKey(parsed.timestamp, totalUsage, lastUsage),
|
||||
eventKey: buildCodexUsageEventKey(
|
||||
// Fork copies rewrite timestamps but keep turn ids; last-only deltas need the timestamp to stay distinct.
|
||||
totalUsage && context.currentTurnId ? context.currentTurnId : parsed.timestamp,
|
||||
totalUsage,
|
||||
lastUsage
|
||||
),
|
||||
cwd: context.currentCwd ?? context.sessionCwd,
|
||||
model: resolvedModel,
|
||||
hasInferredPricing,
|
||||
|
||||
@@ -153,7 +153,7 @@ export function resolveCodexUsageDelta(
|
||||
}
|
||||
|
||||
export function buildCodexUsageEventKey(
|
||||
timestamp: string,
|
||||
recordIdentity: string,
|
||||
totalUsage: CodexUsageRawUsage | null,
|
||||
lastUsage: CodexUsageRawUsage | null
|
||||
): string {
|
||||
@@ -168,5 +168,5 @@ export function buildCodexUsageEventKey(
|
||||
usage.totalTokens
|
||||
].join(',')
|
||||
: ''
|
||||
return [timestamp, tupleOf(totalUsage), tupleOf(lastUsage)].join('|')
|
||||
return [recordIdentity, tupleOf(totalUsage), tupleOf(lastUsage)].join('|')
|
||||
}
|
||||
|
||||
@@ -464,6 +464,40 @@ describe('listCodexSessionFiles', () => {
|
||||
expect(result.processedFiles[1]?.hasDeferredClaims).toBe(true)
|
||||
})
|
||||
|
||||
it('dedupes fork copies whose timestamps Codex rewrote to the fork time', async () => {
|
||||
const sessionsDir = join(userDataDir, 'codex-runtime-home', 'home', 'sessions')
|
||||
mkdirSync(sessionsDir, { recursive: true })
|
||||
const meta = (id: string): string =>
|
||||
`${JSON.stringify({ type: 'session_meta', payload: { id, cwd: join(fakeHomeDir, 'repo') } })}\n`
|
||||
const turn = (turnId: string): string =>
|
||||
`${JSON.stringify({ type: 'turn_context', payload: { turn_id: turnId, cwd: join(fakeHomeDir, 'repo') } })}\n`
|
||||
const parentTurns = (at: string): string =>
|
||||
[turn('turn-1'), usageRecord(at, 10), turn('turn-2'), usageRecord(at, 5, 15)].join('')
|
||||
writeFileSync(
|
||||
join(sessionsDir, 'aaaa-parent.jsonl'),
|
||||
`${meta('parent')}${parentTurns('2026-05-25T12:00:00.000Z')}`
|
||||
)
|
||||
// Each fork copies the parent with rewritten timestamps, then runs an identical new turn.
|
||||
for (const [name, turnId] of [
|
||||
['bbbb-fork', 'turn-b'],
|
||||
['cccc-fork', 'turn-c']
|
||||
]) {
|
||||
writeFileSync(
|
||||
join(sessionsDir, `${name}.jsonl`),
|
||||
`${meta(name)}${meta('parent')}${parentTurns('2026-05-27T12:00:00.000Z')}${turn(turnId)}${usageRecord('2026-05-27T12:01:00.000Z', 7, 22)}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = await scanCodexUsageFiles([], [])
|
||||
|
||||
expect(
|
||||
result.dailyAggregates.reduce((total, aggregate) => total + aggregate.totalTokens, 0)
|
||||
).toBe(29)
|
||||
expect(
|
||||
result.dailyAggregates.find((aggregate) => aggregate.day === '2026-05-27')?.totalTokens
|
||||
).toBe(14)
|
||||
})
|
||||
|
||||
it('does not re-count the cumulative session for copied total-only records', async () => {
|
||||
const sessionsDir = join(userDataDir, 'codex-runtime-home', 'home', 'sessions')
|
||||
mkdirSync(sessionsDir, { recursive: true })
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('CodexUsageStore', () => {
|
||||
} as unknown as CodexUsagePersistedState)
|
||||
|
||||
expect(normalized).toEqual({
|
||||
schemaVersion: 6,
|
||||
schemaVersion: CODEX_USAGE_SCHEMA_VERSION,
|
||||
worktreeFingerprint: null,
|
||||
processedFiles: [],
|
||||
sessions: [],
|
||||
|
||||
@@ -29,6 +29,7 @@ export type CodexUsageParseResumeState = {
|
||||
sessionCwd: string | null
|
||||
currentCwd: string | null
|
||||
currentModel: string | null
|
||||
currentTurnId?: string | null
|
||||
previousTotals: CodexUsageRawUsage | null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user