Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Fixed
- Menu bar: prevent duplicate provider items when usage updates re-enter initial status-item setup (#2162). Thanks @ss251!
- Codex cost usage: count restarted subagent token counters without subtracting the parent's unrelated cumulative baseline (#2193). Thanks @qiuruiyu and @harjothkhara!
- Ollama: explain that API-key verification cannot show Cloud quota limits and direct users to browser-cookie mode (#2159). Thanks @kiranmagic7!
- Claude: suppress duplicate all-model scoped quota rows that could appear as “All models only” beside Weekly.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.

enum CodexParserHash {
static let value = "cdef6eb9658a43e2"
static let value = "f3142eea669758bf"
}
171 changes: 105 additions & 66 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,7 @@ enum CostUsageScanner {
let forkedFromId: String?
let forkTimestamp: String?
let projectPath: String?
let isSubagentThread: Bool
}

private struct CodexTokenCountRecord {
Expand Down Expand Up @@ -1266,6 +1267,8 @@ enum CostUsageScanner {
private static let codexJSONFieldParentSessionId = Array("parent_session_id".utf8)
private static let codexJSONFieldParentSessionIdCamel = Array("parentSessionId".utf8)
private static let codexJSONFieldPayload = Array("payload".utf8)
private static let codexJSONFieldSource = Array("source".utf8)
private static let codexJSONFieldSubagent = Array("subagent".utf8)
private static let codexJSONFieldSessionId = Array("session_id".utf8)
private static let codexJSONFieldSessionIdCamel = Array("sessionId".utf8)
private static let codexJSONFieldTimestamp = Array("timestamp".utf8)
Expand Down Expand Up @@ -1329,6 +1332,47 @@ enum CostUsageScanner {
return nil
}

private static func codexIsSubagentThread(from payload: [String: Any]?) -> Bool {
guard let payload else { return false }
if let source = payload["source"] as? String {
return source.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "subagent"
}
if let source = payload["source"] as? [String: Any] {
return source["subagent"] is String || source["subagent"] is [String: Any]
}
return false
}

private static func codexIsSubagentThread(
from bytes: UnsafeBufferPointer<UInt8>,
in payloadRange: Range<Int>) -> Bool
{
if let source = extractJSONByteStringField(
self.codexJSONFieldSource,
from: bytes,
in: payloadRange,
atDepth: 1)
{
return source.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "subagent"
}
guard let sourceRange = extractJSONByteObjectField(
self.codexJSONFieldSource,
from: bytes,
in: payloadRange,
atDepth: 1)
else { return false }
return extractJSONByteStringField(
self.codexJSONFieldSubagent,
from: bytes,
in: sourceRange,
atDepth: 1) != nil
|| extractJSONByteObjectField(
self.codexJSONFieldSubagent,
from: bytes,
in: sourceRange,
atDepth: 1) != nil
}

private static func codexTurnID(from bytes: UnsafeBufferPointer<UInt8>, in payloadRange: Range<Int>) -> String? {
for key in [self.codexJSONFieldTurnId, self.codexJSONFieldTurnIdCamel, self.codexJSONFieldId] {
if let value = extractJSONByteStringField(key, from: bytes, in: payloadRange, atDepth: 1), !value.isEmpty {
Expand All @@ -1350,21 +1394,24 @@ enum CostUsageScanner {
in rootRange: Range<Int>,
payloadRange: Range<Int>?) -> String?
{
if let payloadRange {
for key in [self.codexJSONFieldSessionId, self.codexJSONFieldSessionIdCamel, self.codexJSONFieldId] {
if let value = extractJSONByteStringField(key, from: bytes, in: payloadRange, atDepth: 1),
!value.isEmpty
{
return value
}
}
}
for key in [Self.codexJSONFieldSessionId, Self.codexJSONFieldSessionIdCamel, Self.codexJSONFieldId] {
if let value = Self.extractJSONByteStringField(key, from: bytes, in: rootRange, atDepth: 1),
!value.isEmpty
{
return value
}
// `session_id` identifies the shared multi-agent tree. `id` identifies this rollout/thread,
// and both fields have appeared at either metadata level.
let candidates: [String?] = [
payloadRange.flatMap {
Self.extractJSONByteStringField(Self.codexJSONFieldId, from: bytes, in: $0, atDepth: 1)
},
Self.extractJSONByteStringField(Self.codexJSONFieldId, from: bytes, in: rootRange, atDepth: 1),
payloadRange.flatMap {
Self.extractJSONByteStringField(Self.codexJSONFieldSessionId, from: bytes, in: $0, atDepth: 1)
},
payloadRange.flatMap {
Self.extractJSONByteStringField(Self.codexJSONFieldSessionIdCamel, from: bytes, in: $0, atDepth: 1)
},
Self.extractJSONByteStringField(Self.codexJSONFieldSessionId, from: bytes, in: rootRange, atDepth: 1),
Self.extractJSONByteStringField(Self.codexJSONFieldSessionIdCamel, from: bytes, in: rootRange, atDepth: 1),
]
for value in candidates where value?.isEmpty == false {
return value
}
return nil
}
Expand Down Expand Up @@ -1445,7 +1492,10 @@ enum CostUsageScanner {
from: rawBuffer,
in: objectRange,
atDepth: 1),
projectPath: Self.codexProjectPath(from: rawBuffer, payloadRange: payloadRange)))
projectPath: Self.codexProjectPath(from: rawBuffer, payloadRange: payloadRange),
isSubagentThread: payloadRange.map {
Self.codexIsSubagentThread(from: rawBuffer, in: $0)
} ?? false))

case "turn_context":
guard let payloadRange = Self.extractJSONByteObjectField(
Expand Down Expand Up @@ -1572,6 +1622,23 @@ enum CostUsageScanner {

static let codexSessionMetadataMaxLineBytes = 256 * 1024

private static func codexSessionMetadata(from obj: [String: Any]) -> CodexSessionMetadata? {
guard obj["type"] as? String == "session_meta" else { return nil }
let payload = obj["payload"] as? [String: Any]
return CodexSessionMetadata(
sessionId: payload?["id"] as? String
?? obj["id"] as? String
?? payload?["session_id"] as? String
?? payload?["sessionId"] as? String
?? obj["session_id"] as? String
?? obj["sessionId"] as? String,
forkedFromId: Self.codexForkParentId(from: payload),
forkTimestamp: payload?["timestamp"] as? String
?? obj["timestamp"] as? String,
projectPath: Self.normalizedCodexProjectPath(payload?["cwd"] as? String),
isSubagentThread: Self.codexIsSubagentThread(from: payload))
}

private static func parseCodexSessionMetadata(
fileURL: URL,
checkCancellation: CancellationCheck? = nil) throws -> CodexSessionMetadata?
Expand All @@ -1598,19 +1665,7 @@ enum CostUsageScanner {
return autoreleasepool {
guard let obj = (try? JSONSerialization.jsonObject(with: lineData)) as? [String: Any]
else { return nil }
guard obj["type"] as? String == "session_meta" else { return nil }
let payload = obj["payload"] as? [String: Any]
return CodexSessionMetadata(
sessionId: payload?["session_id"] as? String
?? payload?["sessionId"] as? String
?? payload?["id"] as? String
?? obj["session_id"] as? String
?? obj["sessionId"] as? String
?? obj["id"] as? String,
forkedFromId: Self.codexForkParentId(from: payload),
forkTimestamp: payload?["timestamp"] as? String
?? obj["timestamp"] as? String,
projectPath: Self.normalizedCodexProjectPath(payload?["cwd"] as? String))
return Self.codexSessionMetadata(from: obj)
}
}

Expand Down Expand Up @@ -1856,6 +1911,7 @@ enum CostUsageScanner {
var sessionId: String?
var forkedFromId: String?
var projectPath: String?
var isSubagentThread = false
var inheritedTotals: CostUsageCodexTotals?
var remainingInheritedTotals: CostUsageCodexTotals?
var forkBaselineResolved = false
Expand Down Expand Up @@ -1912,8 +1968,18 @@ enum CostUsageScanner {
if projectPath == nil {
projectPath = metadata.projectPath
}
isSubagentThread = isSubagentThread || metadata.isSubagentThread
if let forkedFromId {
try resolveForkBaseline(parentSessionId: forkedFromId, forkedAt: metadata.forkTimestamp ?? "")
if isSubagentThread {
// Codex subagent rollouts own an independent cumulative counter; their parent
// identifiers describe lineage, not a token baseline to subtract (#2193).
forkBaselineResolved = true
inheritedTotals = nil
remainingInheritedTotals = nil
hasUnresolvedForkBaseline = false
} else {
try resolveForkBaseline(parentSessionId: forkedFromId, forkedAt: metadata.forkTimestamp ?? "")
}
}
}

Expand Down Expand Up @@ -1954,8 +2020,9 @@ enum CostUsageScanner {
return adjusted
}

// Fork children are measured against the parent-inherited baseline so every cumulative
// comparison in this file happens on a single scale.
// Generic fork children are measured against their parent-inherited baseline so every
// cumulative comparison in this file happens on a single scale. Subagents use their
// independent counter directly.
let adjustedTotal: CostUsageCodexTotals? = total.map { rawTotals in
guard let inheritedTotals, !hasUnresolvedForkBaseline else { return rawTotals }
return CostUsageCodexTotals(
Expand Down Expand Up @@ -2147,15 +2214,7 @@ enum CostUsageScanner {
fileURL: fileURL,
checkCancellation: checkCancellation)
{
sessionId = metadata.sessionId
forkedFromId = metadata.forkedFromId
projectPath = metadata.projectPath
if let forkedFromId = metadata.forkedFromId,
inheritedTotals == nil
{
let forkedAt = metadata.forkTimestamp ?? ""
try resolveForkBaseline(parentSessionId: forkedFromId, forkedAt: forkedAt)
}
try handleSessionMetadata(metadata)
}

var parsedBytes: Int64
Expand Down Expand Up @@ -2208,31 +2267,11 @@ enum CostUsageScanner {
else { return }

if type == "session_meta" {
let payload = obj["payload"] as? [String: Any]
if sessionId == nil {
sessionId = payload?["session_id"] as? String
?? payload?["sessionId"] as? String
?? payload?["id"] as? String
?? obj["session_id"] as? String
?? obj["sessionId"] as? String
?? obj["id"] as? String
}
if forkedFromId == nil {
forkedFromId = Self.codexForkParentId(from: payload)
}
if projectPath == nil {
projectPath = Self.normalizedCodexProjectPath(payload?["cwd"] as? String)
}
if let forkedFromId {
let forkedAt = payload?["timestamp"] as? String
?? obj["timestamp"] as? String
?? ""
do {
try resolveForkBaseline(parentSessionId: forkedFromId, forkedAt: forkedAt)
} catch {
deferredError = error
return
}
guard let metadata = Self.codexSessionMetadata(from: obj) else { return }
do {
try handleSessionMetadata(metadata)
} catch {
deferredError = error
}
return
}
Expand Down
Loading