201 lines · 9.8 KB · Swift
RawDownload
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)    }}