Skip to content

Commit cc5a205

Browse files
committed
fix: preserve conservative subagent parsing
1 parent 97c9ca5 commit cc5a205

4 files changed

Lines changed: 213 additions & 11 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.
22

33
enum CodexParserHash {
4-
static let value = "e4483fa9d0706ab2"
4+
static let value = "aff14fe562b54e8b"
55
}

Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CodexTruncatedPrefix.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
import Foundation
22

33
extension CostUsageScanner {
4+
static func extractCodexTruncatedSessionMetadata(from bytes: Data) ->
5+
(isSessionMetadata: Bool, sessionID: String?)
6+
{
7+
guard let text = truncatedUTF8String(from: bytes) else { return (false, nil) }
8+
let object = text[...]
9+
guard Self.extractJSONStringField("type", from: object, atDepth: 1) == "session_meta" else {
10+
return (false, nil)
11+
}
12+
guard let payloadText = Self.extractJSONObjectField("payload", from: object, atDepth: 1) else {
13+
return (true, nil)
14+
}
15+
let sessionID = Self.extractJSONStringField("id", from: payloadText, atDepth: 1)
16+
?? Self.extractJSONStringField("session_id", from: payloadText, atDepth: 1)
17+
?? Self.extractJSONStringField("sessionId", from: payloadText, atDepth: 1)
18+
return (true, sessionID)
19+
}
20+
421
static func extractCodexTurnContextModel(from bytes: Data) -> String? {
522
guard let text = truncatedUTF8String(from: bytes) else { return nil }
623
let object = text[...]
@@ -44,8 +61,12 @@ extension CostUsageScanner {
4461
case "}":
4562
depth -= 1
4663
text.formIndex(after: &index)
47-
if depth == 0 { return true }
48-
if depth < 0 { return false }
64+
if depth == 0 {
65+
return true
66+
}
67+
if depth < 0 {
68+
return false
69+
}
4970
case "\"":
5071
guard Self.parseJSONString(in: text, index: &index) != nil else { return false }
5172
default:

Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,15 @@ enum CostUsageScanner {
13131313
case interAgentCommunication(triggerTurn: Bool)
13141314
case taskStarted(turnID: String?)
13151315
case tokenCount(CodexTokenCountRecord)
1316+
1317+
var requiresValidTimestamp: Bool {
1318+
switch self {
1319+
case .sessionMeta:
1320+
false
1321+
case .turnContext, .interAgentCommunication, .taskStarted, .tokenCount:
1322+
true
1323+
}
1324+
}
13161325
}
13171326

13181327
private struct CodexBufferedFastLine {
@@ -1704,6 +1713,20 @@ enum CostUsageScanner {
17041713
}
17051714
}
17061715

1716+
private static func codexFastLineTimestampValidity(_ bytes: Data) -> Bool? {
1717+
let timestamp = bytes.withUnsafeBytes { rawBytes in
1718+
let rawBuffer = rawBytes.bindMemory(to: UInt8.self)
1719+
guard !rawBuffer.isEmpty else { return nil as String? }
1720+
return Self.extractJSONByteStringField(
1721+
Self.codexJSONFieldTimestamp,
1722+
from: rawBuffer,
1723+
in: 0..<rawBuffer.count,
1724+
atDepth: 1)
1725+
}
1726+
guard let timestamp else { return nil }
1727+
return (Self.dayKeyFromTimestamp(timestamp) ?? Self.dayKeyFromParsedISO(timestamp)) != nil
1728+
}
1729+
17071730
static func parseCodexSessionIdentifier(
17081731
fileURL: URL,
17091732
checkCancellation: CancellationCheck? = nil) throws -> String?
@@ -2387,6 +2410,23 @@ enum CostUsageScanner {
23872410
deferredError = error
23882411
}
23892412
}
2413+
if pendingSubagentLines != nil {
2414+
let truncatedMetadata = Self.extractCodexTruncatedSessionMetadata(from: line.bytes)
2415+
if truncatedMetadata.isSessionMetadata {
2416+
do {
2417+
try routeFastLine(
2418+
.sessionMeta(CodexSessionMetadata(
2419+
sessionId: truncatedMetadata.sessionID,
2420+
forkedFromId: nil,
2421+
forkTimestamp: nil,
2422+
projectPath: nil,
2423+
isSubagentThread: false)),
2424+
lineIndex: lineIndex)
2425+
} catch {
2426+
deferredError = error
2427+
}
2428+
}
2429+
}
23902430
return
23912431
}
23922432

@@ -2408,12 +2448,20 @@ enum CostUsageScanner {
24082448
}
24092449

24102450
if let fastLine = Self.parseCodexFastLine(line.bytes) {
2411-
do {
2412-
try routeFastLine(fastLine, lineIndex: lineIndex)
2413-
} catch {
2414-
deferredError = error
2451+
let timestampValidity = fastLine.requiresValidTimestamp
2452+
? Self.codexFastLineTimestampValidity(line.bytes)
2453+
: true
2454+
if timestampValidity == true {
2455+
do {
2456+
try routeFastLine(fastLine, lineIndex: lineIndex)
2457+
} catch {
2458+
deferredError = error
2459+
}
2460+
return
2461+
}
2462+
if timestampValidity == false {
2463+
return
24152464
}
2416-
return
24172465
}
24182466

24192467
autoreleasepool {

Tests/CodexBarTests/CodexSubagentAccountingIntegrationTests.swift

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,16 @@ struct CodexSubagentAccountingIntegrationTests {
158158
.replacingOccurrences(
159159
of: "\"type\":\"inter_agent_communication_metadata\"",
160160
with: "\"ty\\u0070e\":\"inter_agent_communication_metadata\""))
161+
let escapedTimestampFileURL = try env.writeCodexSessionFile(
162+
day: day,
163+
filename: "rollout-\(forkTimestamp)-marker-child-escaped-timestamp.jsonl",
164+
contents: fastContents
165+
.replacingOccurrences(of: "marker-child", with: "marker-child-escaped-timestamp")
166+
.replacingOccurrences(of: "\"timestamp\":", with: "\"time\\u0073tamp\":"))
161167

162168
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day)
163169
let normalizedLeafModel = CostUsagePricing.normalizeCodexModel(leafModel)
164-
for fileURL in [fastFileURL, fallbackFileURL] {
170+
for fileURL in [fastFileURL, fallbackFileURL, escapedTimestampFileURL] {
165171
var resolvedParentBaseline = false
166172
let parsed = CostUsageScanner.parseCodexFile(
167173
fileURL: fileURL,
@@ -187,11 +193,11 @@ struct CodexSubagentAccountingIntegrationTests {
187193
until: day,
188194
now: day,
189195
options: options)
190-
#expect(report.data.first?.totalTokens == 110)
196+
#expect(report.data.first?.totalTokens == 165)
191197

192198
let cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)
193199
let childUsages = cache.files.values.filter { $0.sessionId?.hasPrefix("marker-child") == true }
194-
#expect(childUsages.count == 2)
200+
#expect(childUsages.count == 3)
195201
#expect(childUsages.allSatisfy {
196202
$0.forkBaselineDependencyKey == CostUsageScanner.codexForkDependencyNotRequiredKey
197203
})
@@ -261,6 +267,133 @@ struct CodexSubagentAccountingIntegrationTests {
261267
#expect(resolvedParentBaseline)
262268
}
263269

270+
@Test
271+
func `oversized ancestor metadata remains conservative copied-prefix evidence`() throws {
272+
let env = try CostUsageTestEnvironment()
273+
defer { env.cleanup() }
274+
275+
let day = try env.makeLocalNoon(year: 2026, month: 7, day: 16)
276+
let timestamp = env.isoString(for: day)
277+
let opening = try env.jsonl([
278+
[
279+
"type": "session_meta",
280+
"timestamp": timestamp,
281+
"payload": [
282+
"id": "oversized-child",
283+
"source": ["subagent": ["thread_spawn": [:]]],
284+
],
285+
],
286+
self.tokenCount(
287+
timestamp: env.isoString(for: day.addingTimeInterval(1)),
288+
model: "openai/gpt-5.3",
289+
total: (input: 1000, cached: 900, output: 100),
290+
last: (input: 50, cached: 10, output: 5)),
291+
])
292+
let oversizedAncestor = "{\"type\":\"session_meta\",\"timestamp\":\"\(timestamp)\"," +
293+
"\"payload\":{\"id\":\"oversized-parent\",\"padding\":\"" +
294+
String(repeating: "x", count: 300_000) + "\"}}\n"
295+
let tail = try env.jsonl([
296+
self.tokenCount(
297+
timestamp: env.isoString(for: day.addingTimeInterval(2)),
298+
model: "openai/gpt-5.4",
299+
total: (input: 1050, cached: 910, output: 105),
300+
last: (input: 50, cached: 10, output: 5)),
301+
])
302+
let fileURL = try env.writeCodexSessionFile(
303+
day: day,
304+
filename: "rollout-\(timestamp)-oversized-ancestor.jsonl",
305+
contents: opening + oversizedAncestor + tail)
306+
307+
var resolvedParentBaseline = false
308+
let parsed = CostUsageScanner.parseCodexFile(
309+
fileURL: fileURL,
310+
range: CostUsageScanner.CostUsageDayRange(since: day, until: day),
311+
inheritedTotalsResolver: { parentSessionID, _ in
312+
resolvedParentBaseline = true
313+
#expect(parentSessionID == "oversized-parent")
314+
return .resolved(.init(input: 1000, cached: 900, output: 100))
315+
})
316+
317+
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day)
318+
let model = CostUsagePricing.normalizeCodexModel("openai/gpt-5.4")
319+
#expect(parsed.days[dayKey]?[model] == [50, 10, 5])
320+
#expect(parsed.forkedFromId == "oversized-parent")
321+
#expect(parsed.dependsOnParentTotals)
322+
#expect(resolvedParentBaseline)
323+
}
324+
325+
@Test
326+
func `invalid timestamp suffix markers preserve parent dependency on both parser paths`() throws {
327+
let env = try CostUsageTestEnvironment()
328+
defer { env.cleanup() }
329+
330+
let day = try env.makeLocalNoon(year: 2026, month: 7, day: 16)
331+
let timestamp = env.isoString(for: day)
332+
let contents = try env.jsonl([
333+
[
334+
"type": "session_meta",
335+
"timestamp": timestamp,
336+
"payload": [
337+
"id": "invalid-marker-child",
338+
"source": ["subagent": ["thread_spawn": [:]]],
339+
],
340+
],
341+
self.tokenCount(
342+
timestamp: env.isoString(for: day.addingTimeInterval(1)),
343+
model: "openai/gpt-5.3",
344+
total: (input: 1000, cached: 900, output: 100),
345+
last: (input: 50, cached: 10, output: 5)),
346+
[
347+
"type": "session_meta",
348+
"timestamp": timestamp,
349+
"payload": ["id": "invalid-marker-parent"],
350+
],
351+
[
352+
"type": "turn_context",
353+
"payload": ["model": "openai/gpt-5.4"],
354+
],
355+
[
356+
"type": "inter_agent_communication_metadata",
357+
"payload": ["trigger_turn": true],
358+
],
359+
self.tokenCount(
360+
timestamp: env.isoString(for: day.addingTimeInterval(2)),
361+
model: "openai/gpt-5.4",
362+
total: (input: 1050, cached: 910, output: 105),
363+
last: (input: 50, cached: 10, output: 5)),
364+
])
365+
let fastFileURL = try env.writeCodexSessionFile(
366+
day: day,
367+
filename: "rollout-\(timestamp)-invalid-marker.jsonl",
368+
contents: contents)
369+
let fallbackFileURL = try env.writeCodexSessionFile(
370+
day: day,
371+
filename: "rollout-\(timestamp)-invalid-marker-fallback.jsonl",
372+
contents: contents
373+
.replacingOccurrences(of: "invalid-marker-child", with: "invalid-marker-child-fallback")
374+
.replacingOccurrences(of: "\"type\":\"turn_context\"", with: "\"ty\\u0070e\":\"turn_context\"")
375+
.replacingOccurrences(
376+
of: "\"type\":\"inter_agent_communication_metadata\"",
377+
with: "\"ty\\u0070e\":\"inter_agent_communication_metadata\""))
378+
379+
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day)
380+
let model = CostUsagePricing.normalizeCodexModel("openai/gpt-5.4")
381+
for fileURL in [fastFileURL, fallbackFileURL] {
382+
var resolvedParentBaseline = false
383+
let parsed = CostUsageScanner.parseCodexFile(
384+
fileURL: fileURL,
385+
range: CostUsageScanner.CostUsageDayRange(since: day, until: day),
386+
inheritedTotalsResolver: { parentSessionID, _ in
387+
resolvedParentBaseline = true
388+
#expect(parentSessionID == "invalid-marker-parent")
389+
return .resolved(.init(input: 1000, cached: 900, output: 100))
390+
})
391+
#expect(parsed.days[dayKey]?[model] == [50, 10, 5])
392+
#expect(parsed.dependsOnParentTotals)
393+
#expect(resolvedParentBaseline)
394+
}
395+
}
396+
264397
@Test
265398
func `idless copied prefix without a parent or local marker is suppressed`() throws {
266399
let env = try CostUsageTestEnvironment()

0 commit comments

Comments
 (0)