import Foundation /// The event kinds a trace records. `decision` events capture a full policy tick (the /// signals it saw plus what it chose). `menuOpen` and `refreshCompleted` capture the two /// ground-truth events the replay engine anchors a simulation to, independent of any candidate /// policy. `timerAdvanced` captures the one place live behavior *isn't* a plain tick loop: when /// opening the menu makes `UsageStore.noteMenuOpened(at:)` pull the next adaptive refresh forward /// (see `UsageStore.shouldAdvanceAdaptiveTimer(scheduledAt:candidate:)`). Recording it separately /// from `decision` lets a trace answer "did an advance happen, and to when" without relying on /// fragile inference from decision-timestamp gaps. public enum AdaptiveRefreshTraceEventKind: String, Sendable, Codable { case decision case menuOpen case refreshCompleted case timerAdvanced /// Every live advance comparison, including the cases correctly rejected because the current /// timer was already earlier. This is distinct from counterfactual replay advances. case timerAdvanceEvaluated } /// One line of a JSONL adaptive-refresh trace. Field presence depends on `kind`: `decision` /// records populate `menuAgeSeconds`, `lowPowerModeEnabled`, `thermalState`, `reason`, and /// `delaySeconds`, plus optional activity observations supplied by the input trace /// (`codexActivitySeconds`/`claudeActivitySeconds`, the seconds-since-newest-transcript fields, /// and the per-file intensity fields alongside them); timer advance records populate /// `previousScheduledAt`, `candidateScheduledAt`, `reason`, and `delaySeconds`; `menuOpen` and /// `refreshCompleted` carry only `kind` and `timestamp`. public struct AdaptiveRefreshTraceRecord: Sendable, Codable, Equatable { public let kind: AdaptiveRefreshTraceEventKind public let timestamp: Date public let menuAgeSeconds: TimeInterval? public let lowPowerModeEnabled: Bool? public let thermalState: ReplayThermalState? public let reason: String? public let delaySeconds: TimeInterval? /// Timer advance records only: the adaptive timer's scheduled refresh time before the /// comparison, or `nil` when no refresh had been scheduled yet (matches /// `UsageStore.shouldAdvanceAdaptiveTimer`'s "always advance when nothing is scheduled" rule). public let previousScheduledAt: Date? /// Timer advance records only: the candidate refresh time, i.e. the menu-open timestamp plus /// the freshly computed decision's delay. public let candidateScheduledAt: Date? /// `timerAdvanceEvaluated` only: whether the live schedule comparison accepted the candidate. public let timerAdvanceAccepted: Bool? /// `timerAdvanceEvaluated` only: `previousScheduledAt - candidateScheduledAt`, captured before /// whole-second ISO-8601 serialization. Positive means the candidate was earlier. Optional for /// compatibility with traces recorded before exact comparison deltas were added. public let scheduleLeadSeconds: TimeInterval? /// `timerAdvanceEvaluated` only: whether another refresh was in flight at comparison time. public let refreshInFlight: Bool? /// `decision` only: seconds since the newest observed Codex session transcript modification, /// or `nil` when unavailable. Optional so old trace lines without this field keep decoding. public let codexActivitySeconds: TimeInterval? /// `decision` only: the Claude Code counterpart of `codexActivitySeconds`. public let claudeActivitySeconds: TimeInterval? /// `decision` only: how long the newest Codex transcript has been /// growing (its mtime minus its creationDate), or `nil` when unavailable. Not a separate /// session-age field — age is `codexActivitySeconds` + `codexSessionDurationSeconds`. public let codexSessionDurationSeconds: TimeInterval? /// `decision` only: the Claude Code counterpart of `codexSessionDurationSeconds`. public let claudeSessionDurationSeconds: TimeInterval? /// `decision` only: size in bytes of the newest Codex transcript, as a stateless raw value. public let codexTranscriptBytes: Int64? /// `decision` only: the Claude Code counterpart of `codexTranscriptBytes`. public let claudeTranscriptBytes: Int64? /// `decision` only: count of Codex `.jsonl` transcripts modified in the observation window. public let codexActiveTranscriptCount: Int? /// `decision` only: the Claude Code counterpart of `codexActiveTranscriptCount`. public let claudeActiveTranscriptCount: Int? public init( kind: AdaptiveRefreshTraceEventKind, timestamp: Date, menuAgeSeconds: TimeInterval? = nil, lowPowerModeEnabled: Bool? = nil, thermalState: ReplayThermalState? = nil, reason: String? = nil, delaySeconds: TimeInterval? = nil, previousScheduledAt: Date? = nil, candidateScheduledAt: Date? = nil, timerAdvanceAccepted: Bool? = nil, scheduleLeadSeconds: TimeInterval? = nil, refreshInFlight: Bool? = nil, codexActivitySeconds: TimeInterval? = nil, claudeActivitySeconds: TimeInterval? = nil, codexSessionDurationSeconds: TimeInterval? = nil, claudeSessionDurationSeconds: TimeInterval? = nil, codexTranscriptBytes: Int64? = nil, claudeTranscriptBytes: Int64? = nil, codexActiveTranscriptCount: Int? = nil, claudeActiveTranscriptCount: Int? = nil) { self.kind = kind self.timestamp = timestamp self.menuAgeSeconds = menuAgeSeconds self.lowPowerModeEnabled = lowPowerModeEnabled self.thermalState = thermalState self.reason = reason self.delaySeconds = delaySeconds self.previousScheduledAt = previousScheduledAt self.candidateScheduledAt = candidateScheduledAt self.timerAdvanceAccepted = timerAdvanceAccepted self.scheduleLeadSeconds = scheduleLeadSeconds self.refreshInFlight = refreshInFlight self.codexActivitySeconds = codexActivitySeconds self.claudeActivitySeconds = claudeActivitySeconds self.codexSessionDurationSeconds = codexSessionDurationSeconds self.claudeSessionDurationSeconds = claudeSessionDurationSeconds self.codexTranscriptBytes = codexTranscriptBytes self.claudeTranscriptBytes = claudeTranscriptBytes self.codexActiveTranscriptCount = codexActiveTranscriptCount self.claudeActiveTranscriptCount = claudeActiveTranscriptCount } // swiftlint:disable:next function_parameter_count public static func decision( timestamp: Date, menuAgeSeconds: TimeInterval?, lowPowerModeEnabled: Bool, thermalState: ReplayThermalState, reason: String, delaySeconds: TimeInterval, codexActivitySeconds: TimeInterval? = nil, claudeActivitySeconds: TimeInterval? = nil, codexSessionDurationSeconds: TimeInterval? = nil, claudeSessionDurationSeconds: TimeInterval? = nil, codexTranscriptBytes: Int64? = nil, claudeTranscriptBytes: Int64? = nil, codexActiveTranscriptCount: Int? = nil, claudeActiveTranscriptCount: Int? = nil) -> Self { Self( kind: .decision, timestamp: timestamp, menuAgeSeconds: menuAgeSeconds, lowPowerModeEnabled: lowPowerModeEnabled, thermalState: thermalState, reason: reason, delaySeconds: delaySeconds, codexActivitySeconds: codexActivitySeconds, claudeActivitySeconds: claudeActivitySeconds, codexSessionDurationSeconds: codexSessionDurationSeconds, claudeSessionDurationSeconds: claudeSessionDurationSeconds, codexTranscriptBytes: codexTranscriptBytes, claudeTranscriptBytes: claudeTranscriptBytes, codexActiveTranscriptCount: codexActiveTranscriptCount, claudeActiveTranscriptCount: claudeActiveTranscriptCount) } public static func menuOpen(timestamp: Date) -> Self { Self(kind: .menuOpen, timestamp: timestamp) } public static func refreshCompleted(timestamp: Date) -> Self { Self(kind: .refreshCompleted, timestamp: timestamp) } /// - Parameters: /// - timestamp: When the menu open that triggered the advance occurred. /// - previousScheduledAt: The timer's scheduled refresh time immediately before the advance. /// - candidateScheduledAt: The refresh time the timer advanced to (`timestamp + delaySeconds`). /// - reason: The freshly computed decision's reason (e.g. `"recentInteraction"`). /// - delaySeconds: The freshly computed decision's delay. public static func timerAdvanced( timestamp: Date, previousScheduledAt: Date?, candidateScheduledAt: Date, reason: String, delaySeconds: TimeInterval) -> Self { Self( kind: .timerAdvanced, timestamp: timestamp, reason: reason, delaySeconds: delaySeconds, previousScheduledAt: previousScheduledAt, candidateScheduledAt: candidateScheduledAt) } // swiftlint:disable:next function_parameter_count public static func timerAdvanceEvaluated( timestamp: Date, previousScheduledAt: Date?, candidateScheduledAt: Date, reason: String, delaySeconds: TimeInterval, accepted: Bool, refreshInFlight: Bool) -> Self { Self( kind: .timerAdvanceEvaluated, timestamp: timestamp, reason: reason, delaySeconds: delaySeconds, previousScheduledAt: previousScheduledAt, candidateScheduledAt: candidateScheduledAt, timerAdvanceAccepted: accepted, scheduleLeadSeconds: previousScheduledAt.map { $0.timeIntervalSince(candidateScheduledAt) }, refreshInFlight: refreshInFlight) } }