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
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 = "43609cc56f76a003"
static let value = "47144baa8daccf52"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

/// In-memory working set for one bounded scan. Codex persists this shape as normalized
/// `CostUsageStore` rows; Claude and Vertex use their independent compact JSON cache.
struct CostUsageCache: Codable, @unchecked Sendable {
struct CostUsageCache: Codable, Equatable, @unchecked Sendable {
var version: Int = 1
var lastScanUnixMs: Int64 = 0
var scanSinceKey: String?
Expand All @@ -26,7 +26,7 @@ struct CostUsageCache: Codable, @unchecked Sendable {
var roots: [String: Int64]?
}

struct CostUsageCodexActiveLookbackState: Codable {
struct CostUsageCodexActiveLookbackState: Codable, Equatable {
var scanSinceKey: String
var rootPaths: [String]
var nextDayKeyByRoot: [String: String] = [:]
Expand All @@ -35,7 +35,7 @@ struct CostUsageCodexActiveLookbackState: Codable {
var legacyRecursivePendingRootPaths: [String] = []
}

struct CostUsageCodexSessionDiscovery: Codable {
struct CostUsageCodexSessionDiscovery: Codable, Equatable {
struct DirectoryStamp: Codable, Equatable {
var mtimeUnixMs: Int64
var jsonlFileCount: Int
Expand All @@ -47,7 +47,7 @@ struct CostUsageCodexSessionDiscovery: Codable {
var fileId: String?
}

struct HeadScan: Codable {
struct HeadScan: Codable, Equatable {
var path: String
var offset: Int64
var resumeState: CostUsageJsonl.ResumeState?
Expand Down Expand Up @@ -215,7 +215,7 @@ struct CostUsageCodexPreviousReport: Codable, Equatable {
}
}

struct CostUsageFileUsage: Codable {
struct CostUsageFileUsage: Codable, Equatable {
var mtimeUnixMs: Int64
var size: Int64
var days: [String: [String: [Int]]]
Expand Down
8 changes: 4 additions & 4 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsageJsonl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ enum CostUsageJsonl {
}
}

struct ResumeState: Codable {
struct ResumeState: Codable, Equatable {
let offset: Int64
fileprivate let lineStartOffset: Int64
fileprivate let prefix: Data
Expand All @@ -35,8 +35,8 @@ enum CostUsageJsonl {
let resumeState: ResumeState?
}

fileprivate struct JSONTailState: Codable {
private enum ScalarState: Codable {
fileprivate struct JSONTailState: Codable, Equatable {
private enum ScalarState: Codable, Equatable {
case notScalar
case trueLiteral(Int)
case falseLiteral(Int)
Expand All @@ -45,7 +45,7 @@ enum CostUsageJsonl {
case invalid
}

private enum NumberState: Codable {
private enum NumberState: Codable, Equatable {
private enum ByteKind {
case zero
case digit
Expand Down
44 changes: 20 additions & 24 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1710,12 +1710,12 @@ enum CostUsageScanner {
let parsedBytes: Int64
}

enum ClaudePathRole: String, Codable {
enum ClaudePathRole: String, Codable, Equatable {
case parent
case subagent
}

struct ClaudeUsageRow: Codable {
struct ClaudeUsageRow: Codable, Equatable {
let dayKey: String
let model: String
let sessionId: String?
Expand Down Expand Up @@ -2481,7 +2481,7 @@ enum CostUsageScanner {
return String(filename[matchRange])
}

struct CodexSessionMetadata: Codable {
struct CodexSessionMetadata: Codable, Equatable {
let sessionId: String?
let forkedFromId: String?
let forkTimestamp: String?
Expand All @@ -2490,22 +2490,22 @@ enum CostUsageScanner {
let subagentHistoryStartOrdinal: Int?
}

struct CodexTurnContextMetadata: Codable {
struct CodexTurnContextMetadata: Codable, Equatable {
let timestamp: String?
let model: String?
let cwd: String?
let title: String?
}

struct CodexTokenCountRecord: Codable {
struct CodexTokenCountRecord: Codable, Equatable {
let timestamp: String
let model: String?
let turnID: String?
let last: CostUsageCodexTotals?
let total: CostUsageCodexTotals?
}

enum CodexFastLine: Codable {
enum CodexFastLine: Codable, Equatable {
case sessionMeta(CodexSessionMetadata)
case turnContext(CodexTurnContextMetadata)
case interAgentCommunication(triggerTurn: Bool)
Expand All @@ -2522,7 +2522,7 @@ enum CostUsageScanner {
}
}

struct CodexBufferedFastLine: Codable {
struct CodexBufferedFastLine: Codable, Equatable {
let lineIndex: Int
let ordinal: Int?
let endOffset: Int64?
Expand Down Expand Up @@ -4508,22 +4508,6 @@ enum CostUsageScanner {
return previous
}

private static func saveCodexCache(
_ cache: CostUsageCache,
store: CostUsageStore,
options: Options,
range: CostUsageDayRange)
{
// The serial scan queue remains the per-process writer boundary. The store actor owns
// the sole writable connection; app and CLI readers take independent WAL snapshots.
CostUsageStoreAccess.save(
store: store,
cache: cache,
calendar: range.calendar,
requestedScanWindow: (sinceKey: range.scanSinceKey, untilKey: range.scanUntilKey),
reportWindow: (sinceKey: range.sinceKey, untilKey: range.untilKey))
}

// swiftlint:disable:next function_body_length
private static func loadCodexDaily(
range: CostUsageDayRange,
Expand Down Expand Up @@ -4746,7 +4730,19 @@ enum CostUsageScanner {
}
cache.lastScanUnixMs = nowMs
try checkCancellation?()
Self.saveCodexCache(cache, store: loadedCache.store, options: options, range: range)
// The serial scan queue remains the per-process writer boundary. The store actor owns
// the sole writable connection; app and CLI readers take independent WAL snapshots.
let saveResult = CostUsageStoreAccess.save(
store: loadedCache.store,
cache: cache,
calendar: range.calendar,
requestedScanWindow: (sinceKey: range.scanSinceKey, untilKey: range.scanUntilKey),
reportWindow: (sinceKey: range.sinceKey, untilKey: range.untilKey),
skipIdenticalContent: true)
if saveResult.catchUpRequired {
cache.codexScanCatchUpPending = true
cache.codexPreviousReport = previousReport
}
}

if let previous = Self.codexPreviousReport(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,56 @@ extension CostUsageStore {
requestedScanWindow: (sinceKey: String, untilKey: String),
reportWindow: (sinceKey: String, untilKey: String)? = nil,
rowBudget: Int = CostUsageStore.defaultRowBudget,
fileBudgetBytes: Int64 = CostUsageStore.defaultFileBudgetBytes) -> CostUsageStoreBudgetResult
fileBudgetBytes: Int64 = CostUsageStore.defaultFileBudgetBytes,
skipIdenticalContent: Bool = false) -> CostUsageStoreBudgetResult
{
let previous = self.readSnapshot()
if skipIdenticalContent,
Self.persistedContentMatches(
previous: previous,
cache: cache,
calendar: calendar)
{
// Retention owns the safety boundary: even a semantically unchanged scanner result
// must honor newly tightened row/file budgets before it can return.
let result = self.enforceBudgets(
maxRows: rowBudget,
maxFileBytes: fileBudgetBytes,
requestedSinceDay: requestedScanWindow.sinceKey,
requestedUntilDay: requestedScanWindow.untilKey,
calendar: calendar)
guard !result.catchUpRequired else { return result }
Self.identicalContentPreLockCheckpointForTesting?()
guard self.beginSaveTransaction() else {
var retry = result
retry.catchUpRequired = true
return retry
}

// Another process may have committed a full save after the optimistic comparison.
// Recheck the complete semantic snapshot under this writer lock. A mismatch means
// this scanner's cache is stale, so preserve the newer store and request a rescan.
let lockedPrevious = self.readSnapshotInCurrentTransaction()
guard Self.persistedContentMatches(
previous: lockedPrevious,
cache: cache,
calendar: calendar)
else {
_ = self.rollbackSaveTransaction()
var retry = result
retry.catchUpRequired = true
return retry
}

let advanced = self.advanceLastScanUnixMsInCurrentTransaction(cache.lastScanUnixMs)
let committed = self.endSaveTransaction()
guard advanced, committed else {
var retry = result
retry.catchUpRequired = true
return retry
}
return result
}
let canReuseStoredRows = previous.metadata.timeZoneIdentifier == calendar.timeZone.identifier
let previousFilesByPath = Dictionary(uniqueKeysWithValues: previous.files.map { ($0.path, $0) })
let snapshotCountsByPath = previous.tokenSnapshots
Expand All @@ -74,7 +121,13 @@ extension CostUsageStore {
// midway can never leave e.g. files upserted while day_aggregates stay stale.
// Budget enforcement below runs outside: it checkpoints the WAL and vacuums, which
// SQLite forbids inside an open transaction.
self.beginSaveTransaction()
guard self.beginSaveTransaction() else {
return CostUsageStoreBudgetResult(
deletedRows: 0,
rowCount: previous.files.count,
fileBytes: 0,
catchUpRequired: true)
}
self.deleteRemovedFiles(previous: previous, cache: cache)
var persistedFiles = 0
for (path, usage) in cache.files.sorted(by: { $0.key < $1.key }) {
Expand All @@ -94,7 +147,13 @@ extension CostUsageStore {
_ = self.setMetadata(Self.metadata(cache: cache, calendar: calendar))
_ = self.setDiscoveryState(Self.discoveryState(cache.codexSessionDiscovery))
_ = self.setLookbackState(Self.lookbackState(cache.codexActiveLookbackState))
self.endSaveTransaction()
guard self.endSaveTransaction() else {
return CostUsageStoreBudgetResult(
deletedRows: 0,
rowCount: previous.files.count,
fileBytes: 0,
catchUpRequired: true)
}
let result = self.enforceBudgets(
maxRows: rowBudget,
maxFileBytes: fileBudgetBytes,
Expand All @@ -110,6 +169,42 @@ extension CostUsageStore {
}
return result
}

/// True when persisting `cache` would leave every content table semantically unchanged.
/// This is O(persisted cache rows): it reconstructs typed values already read from SQLite
/// and compares them in memory; it never parses timestamps or opens session JSONL files. The
/// persisted spellings of a few optional fields differ from their in-memory forms
/// (`catchUpPending` and `codexScanComplete` store nil as false/true, `timeZoneIdentifier`
/// is fixed by the caller's calendar, and `lastScanUnixMs` is a wall-clock stamp), so
/// those are normalized before the comparison.
private static func persistedContentMatches(
previous: CostUsageStoreSnapshot,
cache: CostUsageCache,
calendar: Calendar) -> Bool
{
var restored = Self.cache(from: previous)
guard restored.timeZoneIdentifier == nil
|| restored.timeZoneIdentifier == calendar.timeZone.identifier
else { return false }
guard (cache.codexScanCatchUpPending ?? false) == restored.codexScanCatchUpPending
else { return false }
// Freshness is the sole ignored semantic field. The time zone is a persistence-derived
// spelling: metadata(cache:calendar:) always writes the caller's calendar identifier.
restored.lastScanUnixMs = cache.lastScanUnixMs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist the refreshed scan timestamp

When an unchanged refresh supplies a newer lastScanUnixMs, this assignment deliberately masks the difference and the early return leaves the old timestamp in SQLite. On the next independently loaded request, makeCodexRefreshPlan compares that stale value against the 60-second refresh interval, so once it expires every subsequent request can rescan the session history instead of being debounced. Persist this freshness marker or change refresh planning so it does not depend on a timestamp discarded here.

Useful? React with 👍 / 👎.

restored.timeZoneIdentifier = calendar.timeZone.identifier
restored.codexScanCatchUpPending = cache.codexScanCatchUpPending
restored.files = restored.files.mapValues(Self.normalizingScanComplete)
var incoming = cache
incoming.timeZoneIdentifier = calendar.timeZone.identifier
incoming.files = incoming.files.mapValues(Self.normalizingScanComplete)
return restored == incoming
}

private static func normalizingScanComplete(_ usage: CostUsageFileUsage) -> CostUsageFileUsage {
var usage = usage
usage.codexScanComplete = usage.codexScanComplete ?? true
return usage
}
}

// MARK: - Cache conversion
Expand Down Expand Up @@ -775,12 +870,14 @@ enum CostUsageStoreAccess {
cache: CostUsageCache,
calendar: Calendar,
requestedScanWindow: (sinceKey: String, untilKey: String),
reportWindow: (sinceKey: String, untilKey: String)? = nil) -> CostUsageStoreBudgetResult
reportWindow: (sinceKey: String, untilKey: String)? = nil,
skipIdenticalContent: Bool = false) -> CostUsageStoreBudgetResult
{
store.syncSaveCodexCache(
cache,
calendar: calendar,
requestedScanWindow: requestedScanWindow,
reportWindow: reportWindow)
reportWindow: reportWindow,
skipIdenticalContent: skipIdenticalContent)
}
}
Loading