Files
2026-07-13 12:22:33 +08:00

202 lines
9.8 KiB
Swift

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)
}
}