mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-13 10:34:32 +00:00
Fix Antigravity provider detection, Codex fork double-counting, and tab ordering
Antigravity: - Handle ephemeral port (--https_server_port 0) via lsof fallback - Force reparse when cached turns are 0 (server may have been unavailable) - Persist precomputed costUSD in session cache for correct pricing - Map gemini-pro-agent to gemini-3.1-pro pricing - Add display names for gemini-pro-agent and gemini-3.5-flash-low Codex: - Detect forked sessions via forked_from_id in session_meta - Skip replayed parent events within 5s of fork creation time - Use parent session ID in dedup key so parent+fork don't double-count Menubar: - Sort provider tabs by cost descending instead of enum declaration order
This commit is contained in:
parent
17eada2aa1
commit
95de63a436
4 changed files with 65 additions and 7 deletions
|
|
@ -120,10 +120,19 @@ struct AgentTabStrip: View {
|
|||
let detectedKeys = Set(
|
||||
todayAll.current.providers.keys.map { $0.lowercased() }
|
||||
)
|
||||
return ProviderFilter.allCases.filter { filter in
|
||||
let detected = ProviderFilter.allCases.filter { filter in
|
||||
if filter == .all { return true }
|
||||
return filter.providerKeys.contains(where: detectedKeys.contains)
|
||||
}
|
||||
let costs = Dictionary(uniqueKeysWithValues: detected.map { ($0, cost(for: $0) ?? 0) })
|
||||
return detected.sorted { a, b in
|
||||
if a == .all { return true }
|
||||
if b == .all { return false }
|
||||
let ca = costs[a, default: 0]
|
||||
let cb = costs[b, default: 0]
|
||||
if ca != cb { return ca > cb }
|
||||
return a.rawValue < b.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
private func cost(for filter: ProviderFilter) -> Double? {
|
||||
|
|
|
|||
|
|
@ -1531,7 +1531,7 @@ function providerCallToCachedCall(call: ParsedProviderCall): CachedCall {
|
|||
webSearchRequests: call.webSearchRequests,
|
||||
cacheCreationOneHourTokens: 0,
|
||||
},
|
||||
costUSD: call.provider === 'mistral-vibe' ? call.costUSD : undefined,
|
||||
costUSD: (call.provider === 'mistral-vibe' || call.provider === 'antigravity') ? call.costUSD : undefined,
|
||||
speed: call.speed,
|
||||
timestamp: call.timestamp,
|
||||
tools: call.tools,
|
||||
|
|
@ -1681,6 +1681,10 @@ function getOrCreateProviderSection(cache: SessionCache, provider: string): Prov
|
|||
}
|
||||
|
||||
function cachedFileNeedsProviderReparse(providerName: string, cached: CachedFile): boolean {
|
||||
// Antigravity data comes from the live server, not from the .pb file.
|
||||
// A 0-turn cache entry may just mean the server was unavailable last run.
|
||||
if (providerName === 'antigravity' && cached.turns.length === 0) return true
|
||||
|
||||
if (providerName !== 'gemini') return false
|
||||
|
||||
return cached.turns.some(turn =>
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ function isLikelyCsrfToken(value: string): boolean {
|
|||
return value.length >= 16 && /^[A-Za-z0-9._~:/+=-]+$/.test(value)
|
||||
}
|
||||
|
||||
export function parseAntigravityServerInfoFromLine(line: string): ServerInfo | null {
|
||||
export function parseAntigravityServerInfoFromLine(line: string): ServerInfo | { port: 0; csrfToken: string } | null {
|
||||
const lower = line.toLowerCase()
|
||||
if (!lower.includes('language_server') || !lower.includes('antigravity')) return null
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ export function parseAntigravityServerInfoFromLine(line: string): ServerInfo | n
|
|||
if (!isLikelyCsrfToken(csrfToken)) return null
|
||||
|
||||
const port = Number(rawPort)
|
||||
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null
|
||||
if (!Number.isInteger(port) || port < 0 || port > 65535) return null
|
||||
|
||||
return { port, csrfToken }
|
||||
}
|
||||
|
|
@ -222,10 +222,36 @@ async function readProcessCommandLines(): Promise<string[]> {
|
|||
return output.split('\n')
|
||||
}
|
||||
|
||||
async function resolveEphemeralPort(csrfToken: string): Promise<ServerInfo | null> {
|
||||
if (process.platform === 'win32') return null
|
||||
try {
|
||||
const pidOutput = await execFileText('pgrep', ['-f', 'language_server.*antigravity'])
|
||||
const pid = pidOutput.trim().split('\n')[0]
|
||||
if (!pid) return null
|
||||
const lsofOutput = await execFileText('lsof', ['-a', '-i', '-P', '-n', '-p', pid])
|
||||
for (const line of lsofOutput.split('\n')) {
|
||||
if (!line.includes('LISTEN')) continue
|
||||
const match = line.match(/:(\d+)\s+\(LISTEN\)/)
|
||||
if (match) {
|
||||
const port = Number(match[1])
|
||||
if (port > 0) return { port, csrfToken }
|
||||
}
|
||||
}
|
||||
} catch { /* best-effort */ }
|
||||
return null
|
||||
}
|
||||
|
||||
async function detectServer(): Promise<ServerInfo | null> {
|
||||
if (cachedServer !== undefined) return cachedServer
|
||||
try {
|
||||
cachedServer = parseAntigravityServerInfo(await readProcessCommandLines())
|
||||
const info = parseAntigravityServerInfo(await readProcessCommandLines())
|
||||
if (info && info.port > 0) {
|
||||
cachedServer = info as ServerInfo
|
||||
} else if (info && info.port === 0) {
|
||||
cachedServer = await resolveEphemeralPort(info.csrfToken)
|
||||
} else {
|
||||
cachedServer = null
|
||||
}
|
||||
return cachedServer
|
||||
} catch { /* process discovery failed or timed out */ }
|
||||
cachedServer = null
|
||||
|
|
@ -291,8 +317,13 @@ async function getModelMap(server: ServerInfo): Promise<ModelMap> {
|
|||
}
|
||||
|
||||
// Strip Antigravity-specific suffixes so the pricing DB can match
|
||||
const PRICING_ALIASES: Record<string, string> = {
|
||||
'gemini-pro': 'gemini-3.1-pro',
|
||||
}
|
||||
|
||||
function normalizePricingModel(model: string): string {
|
||||
return model.replace(/-(high|low|agent)$/, '')
|
||||
const stripped = model.replace(/-(high|low|agent)$/, '')
|
||||
return PRICING_ALIASES[stripped] ?? stripped
|
||||
}
|
||||
|
||||
async function discoverSessions(): Promise<SessionSource[]> {
|
||||
|
|
@ -424,11 +455,13 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
|
|||
}
|
||||
|
||||
const modelDisplayNames: Record<string, string> = {
|
||||
'gemini-pro-agent': 'Gemini Pro',
|
||||
'gemini-3-pro': 'Gemini 3 Pro',
|
||||
'gemini-3.1-pro-high': 'Gemini 3.1 Pro',
|
||||
'gemini-3.1-pro-low': 'Gemini 3.1 Pro (Low)',
|
||||
'gemini-3-flash': 'Gemini 3 Flash',
|
||||
'gemini-3-flash-agent': 'Gemini 3 Flash',
|
||||
'gemini-3.5-flash-low': 'Gemini 3.5 Flash',
|
||||
'gemini-3.1-flash-image': 'Gemini 3.1 Flash',
|
||||
'gemini-3.1-flash-lite': 'Gemini 3.1 Flash Lite',
|
||||
'claude-opus-4-6-thinking': 'Opus 4.6',
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ type CodexEntry = {
|
|||
model_provider?: string
|
||||
originator?: string
|
||||
session_id?: string
|
||||
forked_from_id?: string
|
||||
model?: string
|
||||
name?: string
|
||||
content?: Array<{ type?: string; text?: string }>
|
||||
|
|
@ -224,6 +225,7 @@ function parseCodexLine(line: string | Buffer): CodexEntry | null {
|
|||
model_provider: getRawJsonStringField(pHead, 'model_provider'),
|
||||
originator: getRawJsonStringField(pHead, 'originator'),
|
||||
session_id: getRawJsonStringField(pHead, 'session_id'),
|
||||
forked_from_id: getRawJsonStringField(pHead, 'forked_from_id'),
|
||||
model: getRawJsonStringField(pHead, 'model'),
|
||||
name: getRawJsonStringField(pHead, 'name'),
|
||||
},
|
||||
|
|
@ -315,6 +317,8 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
|
|||
|
||||
let sessionModel: string | undefined
|
||||
let sessionId = ''
|
||||
let forkedFromId = ''
|
||||
let forkCutoff = ''
|
||||
// Null sentinel rather than `0` so the FIRST event is never confused
|
||||
// with a duplicate. A session that only emits last_token_usage (no
|
||||
// total_token_usage) reports cumulativeTotal=0 on every event; with a
|
||||
|
|
@ -345,6 +349,10 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
|
|||
|
||||
if (entry.type === 'session_meta') {
|
||||
sessionId = entry.payload?.session_id ?? basename(source.path, '.jsonl')
|
||||
forkedFromId = entry.payload?.forked_from_id ?? ''
|
||||
if (forkedFromId && entry.timestamp) {
|
||||
forkCutoff = new Date(new Date(entry.timestamp).getTime() + 5000).toISOString()
|
||||
}
|
||||
sessionModel = entry.payload?.model ?? sessionModel
|
||||
continue
|
||||
}
|
||||
|
|
@ -383,6 +391,10 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
|
|||
}
|
||||
|
||||
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count') {
|
||||
// Forked sessions replay the parent's entire event history with
|
||||
// timestamps clustered at the fork creation time. Skip replayed
|
||||
// events (within 5s of fork) to avoid double-counting.
|
||||
if (forkCutoff && entry.timestamp && entry.timestamp < forkCutoff) continue
|
||||
const info = entry.payload.info
|
||||
if (!info) {
|
||||
if (pendingOutputChars === 0 && pendingUserMessage.length === 0) continue
|
||||
|
|
@ -479,7 +491,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
|
|||
|
||||
const model = resolveModel(entry.payload, sessionModel)
|
||||
const timestamp = entry.timestamp ?? ''
|
||||
const dedupKey = `codex:${sessionId}:${timestamp}:${cumulativeTotal}`
|
||||
const dedupKey = `codex:${forkedFromId || sessionId}:${cumulativeTotal}`
|
||||
|
||||
if (seenKeys.has(dedupKey)) continue
|
||||
seenKeys.add(dedupKey)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue