Files
steipete--codexbar/Sources/CodexBar/UsageStore+OpenAIWeb.swift
T
2026-07-13 12:22:33 +08:00

1568 lines
69 KiB
Swift

import CodexBarCore
import Foundation
struct OpenAIWebRefreshGateContext {
let force: Bool
let accountDidChange: Bool
let lastError: String?
let lastSnapshotAt: Date?
let lastAttemptAt: Date?
let now: Date
let refreshInterval: TimeInterval
}
struct OpenAIWebRefreshPolicyContext {
let accessEnabled: Bool
let batterySaverEnabled: Bool
let force: Bool
let refreshPhase: ProviderRefreshPhase
}
// MARK: - OpenAI web lifecycle
extension UsageStore {
private struct OpenAIDashboardRefreshContext {
let targetEmail: String?
let allowCurrentSnapshotFallback: Bool
let expectedGuard: CodexAccountScopedRefreshGuard?
let refreshTaskToken: UUID
let allowCodexUsageBackfill: Bool
let force: Bool
}
private struct OpenAIDashboardCookieImportRequest {
let normalizedTarget: String?
let allowAnyAccount: Bool
let cookieSource: ProviderCookieSource
let cacheScope: CookieHeaderCache.Scope?
let preferCachedCookieHeader: Bool?
let force: Bool
}
private static let openAIWebRefreshMultiplier: TimeInterval = 5
private static let openAIWebPrimaryFetchTimeout: TimeInterval = 25
private static let openAIWebRetryFetchTimeout: TimeInterval = 8
private static let openAIWebPostImportFetchTimeout: TimeInterval = 25
static func openAIWebDashboardFetchTimeout(didImportCookies: Bool) -> TimeInterval {
didImportCookies ? self.openAIWebPostImportFetchTimeout : self.openAIWebPrimaryFetchTimeout
}
static func openAIWebRetryDashboardFetchTimeout(afterCookieImport: Bool) -> TimeInterval {
afterCookieImport ? self.openAIWebPostImportFetchTimeout : self.openAIWebRetryFetchTimeout
}
nonisolated static func refreshPhase(
hasCompletedInitialRefresh: Bool) -> ProviderRefreshPhase
{
hasCompletedInitialRefresh ? .regular : .startup
}
nonisolated static func openAIWebRefreshPhase(
providerRefreshPhase: ProviderRefreshPhase,
startupConnectivityRetryAttempt: Int?) -> ProviderRefreshPhase
{
startupConnectivityRetryAttempt == nil ? providerRefreshPhase : .startup
}
func openAIWebRefreshIntervalSeconds() -> TimeInterval {
let base = max(self.normalRefreshIntervalForHeuristics() ?? 0, 120)
return base * Self.openAIWebRefreshMultiplier
}
func requestOpenAIDashboardRefreshIfStale(reason: String) {
guard self.isEnabled(.codex),
self.settings.openAIWebAccessEnabled,
self.settings.codexCookieSource.isEnabled
else { return }
let now = Date()
let refreshInterval = self.openAIWebRefreshIntervalSeconds()
let dashboard = self.openAIDashboard ?? self.lastOpenAIDashboardSnapshot
let lastUpdatedAt = dashboard?.updatedAt
let needsMenuHistoryRefresh = dashboard?.dailyBreakdown.isEmpty == true &&
dashboard?.usageBreakdown.isEmpty == true
if needsMenuHistoryRefresh,
Self.shouldSkipOpenAIWebEmptyHistoryRetry(.init(
force: false,
accountDidChange: self.openAIWebAccountDidChange,
lastError: self.lastOpenAIDashboardError,
lastSnapshotAt: lastUpdatedAt,
lastAttemptAt: self.lastOpenAIDashboardAttemptAt,
now: now,
refreshInterval: refreshInterval))
{
return
}
if let lastUpdatedAt, now.timeIntervalSince(lastUpdatedAt) < refreshInterval, !needsMenuHistoryRefresh {
return
}
let stamp = now.formatted(date: .abbreviated, time: .shortened)
self.logOpenAIWeb("[\(stamp)] OpenAI web refresh request: \(reason)")
let forceRefresh = Self.forceOpenAIWebRefreshForStaleRequest(
batterySaverEnabled: self.settings.openAIWebBatterySaverEnabled) || needsMenuHistoryRefresh
self.openAIWebLogger.info(
"OpenAI web stale refresh gate",
metadata: [
"reason": reason,
"force": forceRefresh ? "1" : "0",
"batterySaverEnabled": self.settings.openAIWebBatterySaverEnabled ? "1" : "0",
"interaction": ProviderInteractionContext.current == .userInitiated ? "user" : "background",
])
let expectedGuard = self.freshCodexOpenAIWebRefreshGuard()
Task { await self.refreshOpenAIDashboardIfNeeded(force: forceRefresh, expectedGuard: expectedGuard) }
}
func applyOpenAIDashboard(
_ dash: OpenAIDashboardSnapshot,
targetEmail: String?,
expectedGuard: CodexAccountScopedRefreshGuard? = nil,
refreshTaskToken: UUID? = nil,
allowCodexUsageBackfill: Bool = true) async
{
guard self.shouldApplyOpenAIDashboardRefreshTask(token: refreshTaskToken) else { return }
self.settings.invalidateCodexAccountReconciliationSnapshotCache()
let authority = self.evaluateCodexDashboardAuthority(
dashboard: dash,
sourceKind: .liveWeb,
routingTargetEmail: targetEmail)
if let expectedGuard {
let shouldApply = switch authority.decision.disposition {
case .attach:
self.shouldApplyOpenAIDashboardRefreshGuard(
expectedGuard: expectedGuard,
routingTargetEmail: targetEmail)
case .displayOnly, .failClosed:
self.shouldApplyOpenAIDashboardPolicyResult(
expectedGuard: expectedGuard,
routingTargetEmail: targetEmail)
}
guard shouldApply else { return }
}
let attachedAccountEmail = self.codexDashboardAttachmentEmail(from: authority.input)
self.reconcileCodexPublishedUsageOwner(with: self.freshCodexAccountScopedRefreshGuard(
preferCurrentSnapshot: true,
allowLastKnownLiveFallback: false))
await self.applyOpenAIDashboardAuthorityDecision(
authority.decision,
dashboard: dash,
authorityInput: authority.input,
attachedAccountEmail: attachedAccountEmail,
allowCodexUsageBackfill: allowCodexUsageBackfill)
}
func applyOpenAIDashboardFailure(
message: String,
expectedGuard: CodexAccountScopedRefreshGuard? = nil,
refreshTaskToken: UUID? = nil,
routingTargetEmail: String? = nil) async
{
guard self.shouldApplyOpenAIDashboardRefreshTask(token: refreshTaskToken) else { return }
if let expectedGuard,
!self.shouldApplyOpenAIWebNonSuccessResult(
expectedGuard: expectedGuard,
routingTargetEmail: routingTargetEmail)
{
return
}
if self.openAIWebManagedTargetStoreIsUnreadable() {
await self.failClosedRefreshForUnreadableManagedCodexStore()
return
}
if self.openAIWebManagedTargetIsMissing() {
await self.failClosedRefreshForMissingManagedCodexTarget()
return
}
if self.openAIWebProfileTargetEmailIsMissing() {
await self.failClosedRefreshForMissingProfileCodexTarget()
return
}
OpenAIDashboardFetcher.evictAllCachedWebViews()
await MainActor.run {
if let cached = self.lastOpenAIDashboardSnapshot {
self.openAIDashboard = cached
self.openAIDashboardAttachmentAuthorized = self.lastOpenAIDashboardAttachmentAuthorized
let stamp = cached.updatedAt.formatted(date: .abbreviated, time: .shortened)
self.lastOpenAIDashboardError =
"Last OpenAI dashboard refresh failed: \(message). Cached values from \(stamp)."
} else {
self.lastOpenAIDashboardError = message
self.openAIDashboard = nil
self.openAIDashboardAttachmentAuthorized = false
}
}
}
func applyOpenAIDashboardLoginRequiredFailure(
expectedGuard: CodexAccountScopedRefreshGuard? = nil,
refreshTaskToken: UUID? = nil,
routingTargetEmail: String? = nil) async
{
guard self.shouldApplyOpenAIDashboardRefreshTask(token: refreshTaskToken) else { return }
if let expectedGuard,
!self.shouldApplyOpenAIWebNonSuccessResult(
expectedGuard: expectedGuard,
routingTargetEmail: routingTargetEmail)
{
return
}
if self.openAIWebManagedTargetStoreIsUnreadable() {
await self.failClosedRefreshForUnreadableManagedCodexStore()
return
}
if self.openAIWebManagedTargetIsMissing() {
await self.failClosedRefreshForMissingManagedCodexTarget()
return
}
if self.openAIWebProfileTargetEmailIsMissing() {
await self.failClosedRefreshForMissingProfileCodexTarget()
return
}
OpenAIDashboardFetcher.evictAllCachedWebViews()
await MainActor.run {
self.lastOpenAIDashboardError = [
"OpenAI web access requires a signed-in chatgpt.com session.",
"Sign in using \(self.codexBrowserCookieOrder.loginHint), " +
"then update OpenAI cookies in Providers → Codex.",
].joined(separator: " ")
self.openAIDashboard = self.lastOpenAIDashboardSnapshot
self.openAIDashboardAttachmentAuthorized = self.lastOpenAIDashboardAttachmentAuthorized
self.openAIDashboardRequiresLogin = true
}
}
private func failClosedOpenAIDashboardSnapshot() {
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
self.openAIDashboardRequiresLogin = true
}
private func applyOpenAIDashboardAuthorityDecision(
_ decision: CodexDashboardAuthorityDecision,
dashboard: OpenAIDashboardSnapshot,
authorityInput: CodexDashboardAuthorityInput,
attachedAccountEmail: String?,
allowCodexUsageBackfill: Bool) async
{
switch decision.disposition {
case .attach:
self.openAIDashboard = dashboard
self.openAIDashboardAttachmentAuthorized = true
self.lastOpenAIDashboardSnapshot = dashboard
self.lastOpenAIDashboardAttachmentAuthorized = true
self.lastOpenAIDashboardError = nil
self.openAIDashboardRequiresLogin = false
if decision.allowedEffects.contains(.usageBackfill),
allowCodexUsageBackfill,
self.snapshots[.codex] == nil,
let usage = dashboard.toUsageSnapshot(provider: .codex, accountEmail: attachedAccountEmail),
CodexWeeklyResetConfirmation.initialDecision(previous: nil, initial: usage) == .publishInitial
{
self.snapshots[.codex] = usage
self.errors[.codex] = nil
self.failureGates[.codex]?.recordSuccess()
self.lastSourceLabels[.codex] = "openai-web"
self.lastCodexUsagePublicationGuard = self.currentCodexAccountScopedRefreshGuard(
preferCurrentSnapshot: true,
allowLastKnownLiveFallback: false)
}
if decision.allowedEffects.contains(.creditsAttachment),
self.credits == nil,
let credits = dashboard.toCreditsSnapshot()
{
self.credits = credits
self.lastCreditsSnapshot = credits
self.lastCreditsSnapshotAccountKey = Self.normalizeCodexAccountScopedKey(attachedAccountEmail)
self.lastCreditsSource = .dashboardWeb
self.lastCreditsError = nil
self.creditsFailureStreak = 0
}
if decision.allowedEffects.contains(.refreshGuardSeed) {
self.seedCodexAccountScopedRefreshGuard(accountEmail: attachedAccountEmail)
}
if let attachedAccountEmail, !attachedAccountEmail.isEmpty {
OpenAIDashboardCacheStore.save(OpenAIDashboardCache(
accountEmail: attachedAccountEmail,
snapshot: dashboard))
}
if decision.allowedEffects.contains(.historicalBackfill) {
self.backfillCodexHistoricalFromDashboardIfNeeded(
dashboard,
authorityDecision: decision,
attachedAccountEmail: attachedAccountEmail)
}
case .displayOnly:
self.applyOpenAIDashboardCleanup(decision.cleanup, preserveVisibleDashboard: true)
self.openAIDashboard = dashboard
self.openAIDashboardAttachmentAuthorized = false
self.lastOpenAIDashboardSnapshot = dashboard
self.lastOpenAIDashboardAttachmentAuthorized = false
self.lastOpenAIDashboardError = nil
self.openAIDashboardRequiresLogin = false
case .failClosed:
self.applyOpenAIDashboardCleanup(decision.cleanup, preserveVisibleDashboard: false)
self.lastOpenAIDashboardError = self.openAIDashboardPolicyFailureMessage(
for: decision,
authorityInput: authorityInput)
self.openAIDashboardRequiresLogin = true
}
}
private func applyOpenAIDashboardCleanup(
_ cleanup: Set<CodexDashboardCleanup>,
preserveVisibleDashboard: Bool)
{
if cleanup.contains(.dashboardDerivedUsage) {
self.clearDashboardDerivedCodexUsageIfNeeded()
}
if cleanup.contains(.dashboardDerivedCredits) {
self.clearDashboardDerivedCreditsIfNeeded()
}
if cleanup.contains(.dashboardRefreshGuardSeed) {
self.clearDashboardRefreshGuardSeedIfNeeded()
}
if cleanup.contains(.dashboardCache) {
OpenAIDashboardCacheStore.clear()
}
if cleanup.contains(.dashboardSnapshot), !preserveVisibleDashboard {
self.openAIDashboard = nil
self.openAIDashboardAttachmentAuthorized = false
self.lastOpenAIDashboardSnapshot = nil
self.lastOpenAIDashboardAttachmentAuthorized = false
}
}
private func clearDashboardDerivedCodexUsageIfNeeded() {
guard self.lastSourceLabels[.codex] == "openai-web" else { return }
self.clearCodexPublishedUsageState()
}
private func clearDashboardDerivedCreditsIfNeeded() {
guard self.lastCreditsSource == .dashboardWeb else { return }
self.credits = nil
self.lastCreditsError = nil
self.lastCreditsSnapshot = nil
self.lastCreditsSnapshotAccountKey = nil
self.lastCreditsSource = .none
self.creditsFailureStreak = 0
}
private func clearDashboardRefreshGuardSeedIfNeeded() {
let currentGuard = self.currentCodexAccountScopedRefreshGuard(
preferCurrentSnapshot: false,
allowLastKnownLiveFallback: false)
if self.snapshots[.codex] != nil,
self.lastCodexUsagePublicationGuard.map({
!Self.codexScopedRefreshGuardsMatchAccount($0, currentGuard)
}) ?? true
{
self.clearCodexPublishedUsageState()
}
self.lastCodexAccountScopedRefreshGuard = currentGuard
}
private func openAIDashboardPolicyFailureMessage(
for decision: CodexDashboardAuthorityDecision,
authorityInput: CodexDashboardAuthorityInput) -> String
{
switch decision.reason {
case let .wrongEmail(expected, actual):
[
"OpenAI dashboard signed in as \(actual ?? "unknown"), but Codex uses \(expected ?? "unknown").",
"Switch accounts in your browser and update OpenAI cookies in Providers → Codex.",
].joined(separator: " ")
case let .sameEmailAmbiguity(email):
"OpenAI dashboard ownership is ambiguous for \(email); Codex will not attach dashboard data."
case .missingDashboardSignedInEmail:
"OpenAI dashboard did not report a signed-in account. Refresh OpenAI cookies and try again."
case .unresolvedWithoutTrustedEvidence:
"OpenAI dashboard ownership could not be verified for the active Codex account."
case .providerAccountMissingScopedEmail:
"Codex account ownership could not be verified because the scoped email is unavailable."
case .providerAccountLacksExactOwnershipProof:
[
"OpenAI dashboard ownership could not be matched to the active Codex account.",
"Refresh Codex account data, then retry OpenAI web access.",
].joined(separator: " ")
case .exactProviderAccountMatch,
.trustedEmailMatchNoCompetingOwner,
.trustedContinuityNoCompetingOwner:
"OpenAI dashboard ownership policy blocked this dashboard."
}
}
func refreshOpenAIDashboardIfNeeded(
force: Bool = false,
expectedGuard: CodexAccountScopedRefreshGuard? = nil,
bypassCoalescing: Bool = false,
allowCodexUsageBackfill: Bool = true) async
{
self.syncOpenAIWebState()
guard self.isEnabled(.codex),
self.settings.openAIWebAccessEnabled,
self.settings.codexCookieSource.isEnabled
else { return }
if self.openAIWebManagedTargetStoreIsUnreadable() {
await self.failClosedRefreshForUnreadableManagedCodexStore()
return
}
if self.openAIWebManagedTargetIsMissing() {
await self.failClosedRefreshForMissingManagedCodexTarget()
return
}
if self.openAIWebProfileTargetEmailIsMissing() {
await self.failClosedRefreshForMissingProfileCodexTarget()
return
}
let allowCurrentSnapshotFallback = expectedGuard?.source == .liveSystem && expectedGuard?
.identity == .unresolved
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
allowCurrentSnapshotFallback: allowCurrentSnapshotFallback,
allowLastKnownLiveFallback: expectedGuard?.identity != .unresolved)
let refreshKey = self.openAIDashboardRefreshKey(targetEmail: targetEmail, expectedGuard: expectedGuard)
if !bypassCoalescing,
let task = self.openAIDashboardRefreshTask,
self.openAIDashboardRefreshTaskKey == refreshKey
{
await task.value
return
}
if bypassCoalescing {
self.openAIDashboardBackgroundRefreshTask?.cancel()
self.openAIDashboardRefreshTask?.cancel()
}
self.handleOpenAIWebTargetEmailChangeIfNeeded(
targetEmail: targetEmail,
targetScope: self.codexCookieCacheScopeForOpenAIWeb())
let now = Date()
let minInterval = self.openAIWebRefreshIntervalSeconds()
let refreshGate = OpenAIWebRefreshGateContext(
force: force,
accountDidChange: self.openAIWebAccountDidChange,
lastError: self.lastOpenAIDashboardError,
lastSnapshotAt: self.lastOpenAIDashboardSnapshot?.updatedAt,
lastAttemptAt: self.lastOpenAIDashboardAttemptAt,
now: now,
refreshInterval: minInterval)
if Self.shouldSkipOpenAIWebRefresh(refreshGate) {
return
}
self.lastOpenAIDashboardAttemptAt = now
let taskToken = UUID()
let context = OpenAIDashboardRefreshContext(
targetEmail: targetEmail,
allowCurrentSnapshotFallback: allowCurrentSnapshotFallback,
expectedGuard: expectedGuard,
refreshTaskToken: taskToken,
allowCodexUsageBackfill: allowCodexUsageBackfill,
force: force)
let task = Task { [weak self] in
guard let self else { return }
await self.performOpenAIDashboardRefreshIfNeeded(context)
}
self.openAIDashboardRefreshTask = task
self.openAIDashboardRefreshTaskKey = refreshKey
self.openAIDashboardRefreshTaskToken = taskToken
await task.value
if self.openAIDashboardRefreshTaskToken == taskToken {
self.openAIDashboardRefreshTask = nil
self.openAIDashboardRefreshTaskKey = nil
self.openAIDashboardRefreshTaskToken = nil
}
}
func scheduleOpenAIDashboardRefreshIfNeeded(expectedGuard: CodexAccountScopedRefreshGuard? = nil) {
self.syncOpenAIWebState()
let allowCurrentSnapshotFallback = expectedGuard?.source == .liveSystem && expectedGuard?
.identity == .unresolved
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
allowCurrentSnapshotFallback: allowCurrentSnapshotFallback,
allowLastKnownLiveFallback: expectedGuard?.identity != .unresolved)
let refreshKey = self.openAIDashboardRefreshKey(targetEmail: targetEmail, expectedGuard: expectedGuard)
if let task = self.openAIDashboardBackgroundRefreshTask,
!task.isCancelled,
self.openAIDashboardBackgroundRefreshTaskKey == refreshKey
{
return
}
if self.openAIDashboardBackgroundRefreshTaskKey != nil,
self.openAIDashboardBackgroundRefreshTaskKey != refreshKey
{
self.invalidateOpenAIDashboardRefreshTask()
}
self.openAIDashboardBackgroundRefreshTask?.cancel()
self.openAIDashboardBackgroundRefreshTaskKey = refreshKey
self.openAIDashboardBackgroundRefreshTask = Task(priority: .utility) { @MainActor [weak self] in
guard let self else { return }
defer {
if self.openAIDashboardBackgroundRefreshTaskKey == refreshKey {
self.openAIDashboardBackgroundRefreshTask = nil
self.openAIDashboardBackgroundRefreshTaskKey = nil
}
}
guard !Task.isCancelled else { return }
await self.refreshOpenAIDashboardIfNeeded(force: false, expectedGuard: expectedGuard)
guard !Task.isCancelled else { return }
self.persistWidgetSnapshot(reason: "dashboard")
}
}
private func performOpenAIDashboardRefreshIfNeeded(_ context: OpenAIDashboardRefreshContext) async {
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
self.openAIDashboardCookieImportStatus = nil
var latestCookieImportStatus: String?
if self.openAIWebDebugLines.isEmpty {
self.resetOpenAIWebDebugLog(context: "refresh")
} else {
let stamp = Date().formatted(date: .abbreviated, time: .shortened)
self.logOpenAIWeb("[\(stamp)] OpenAI web refresh start")
}
let log: (String) -> Void = { [weak self] line in
guard let self else { return }
self.logOpenAIWeb(line)
}
do {
let normalized = context.targetEmail?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
var effectiveEmail = context.targetEmail
// Use a per-email persistent `WKWebsiteDataStore` so multiple dashboard sessions can coexist.
// Strategy:
// - Try the existing per-email WebKit cookie store first (fast; avoids Keychain prompts).
// - On login-required or account mismatch, import cookies from the configured browser order and retry once.
var didImportCookiesForRefresh = false
if self.openAIWebAccountDidChange, let targetEmail = context.targetEmail, !targetEmail.isEmpty {
// On account switches, proactively re-import cookies so we don't show stale data from the previous
// user.
let imported = await self.importOpenAIDashboardCookiesIfNeeded(
targetEmail: targetEmail,
force: true)
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
didImportCookiesForRefresh = true
latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
if await self.abortOpenAIDashboardRetryAfterImportFailure(
importedEmail: imported,
targetEmail: targetEmail,
expectedGuard: context.expectedGuard,
cookieImportStatus: latestCookieImportStatus,
refreshTaskToken: context.refreshTaskToken)
{
self.openAIWebAccountDidChange = false
return
}
if let imported {
effectiveEmail = imported
}
self.openAIWebAccountDidChange = false
}
var dash = try await self.loadLatestOpenAIDashboard(
accountEmail: effectiveEmail,
logger: log,
allowNavigationTimeoutRetry: context.force,
timeout: Self.openAIWebDashboardFetchTimeout(didImportCookies: didImportCookiesForRefresh))
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
if self.dashboardEmailMismatch(expected: normalized, actual: dash.signedInEmail) {
if let imported = await self.importOpenAIDashboardCookiesIfNeeded(
targetEmail: context.targetEmail,
force: true)
{
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
effectiveEmail = imported
}
latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
dash = try await self.loadLatestOpenAIDashboard(
accountEmail: effectiveEmail,
logger: log,
allowNavigationTimeoutRetry: context.force,
timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true))
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
}
await self.applyOpenAIDashboard(
dash,
targetEmail: effectiveEmail,
expectedGuard: context.expectedGuard,
refreshTaskToken: context.refreshTaskToken,
allowCodexUsageBackfill: context.allowCodexUsageBackfill)
} catch let OpenAIDashboardFetcher.FetchError.noDashboardData(body) {
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
await self.retryOpenAIDashboardAfterNoData(
body: body,
context: context,
latestCookieImportStatus: &latestCookieImportStatus,
logger: log)
} catch OpenAIDashboardFetcher.FetchError.loginRequired {
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
await self.retryOpenAIDashboardAfterLoginRequired(
context: context,
latestCookieImportStatus: &latestCookieImportStatus,
logger: log)
} catch {
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
if Self.isOpenAIDashboardTimeout(error) {
await self.retryOpenAIDashboardAfterTimeout(
context: context,
latestCookieImportStatus: &latestCookieImportStatus,
logger: log)
return
}
let message = self.preferredOpenAIDashboardFailureMessage(
error: error,
targetEmail: context.targetEmail,
cookieImportStatus: latestCookieImportStatus)
await self.applyOpenAIDashboardFailure(
message: message,
expectedGuard: context.expectedGuard,
refreshTaskToken: context.refreshTaskToken,
routingTargetEmail: context.targetEmail)
}
}
private func retryOpenAIDashboardAfterTimeout(
context: OpenAIDashboardRefreshContext,
latestCookieImportStatus: inout String?,
logger: @escaping (String) -> Void) async
{
if !context.force {
OpenAIDashboardFetcher.evictAllCachedWebViews()
logger("OpenAI web refresh timed out; skipping immediate background retry.")
await self.applyOpenAIDashboardFailure(
message: L(
"OpenAI web dashboard refresh timed out. CodexBar will retry after the refresh cooldown."),
expectedGuard: context.expectedGuard,
refreshTaskToken: context.refreshTaskToken,
routingTargetEmail: context.targetEmail)
return
}
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
allowCurrentSnapshotFallback: context.allowCurrentSnapshotFallback,
allowLastKnownLiveFallback: context.expectedGuard?.identity != .unresolved)
var effectiveEmail = targetEmail
let imported = await self.importOpenAIDashboardCookiesIfNeeded(
targetEmail: targetEmail,
force: true,
preferCachedCookieHeader: true)
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
if await self.abortOpenAIDashboardRetryAfterImportFailure(
importedEmail: imported,
targetEmail: targetEmail,
expectedGuard: context.expectedGuard,
cookieImportStatus: latestCookieImportStatus,
refreshTaskToken: context.refreshTaskToken)
{
return
}
if let imported {
effectiveEmail = imported
}
do {
let dash = try await self.loadLatestOpenAIDashboard(
accountEmail: effectiveEmail,
logger: logger,
allowNavigationTimeoutRetry: context.force,
timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true))
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
await self.applyOpenAIDashboard(
dash,
targetEmail: effectiveEmail,
expectedGuard: context.expectedGuard,
refreshTaskToken: context.refreshTaskToken,
allowCodexUsageBackfill: context.allowCodexUsageBackfill)
} catch {
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
let message = self.preferredOpenAIDashboardFailureMessage(
error: error,
targetEmail: targetEmail,
cookieImportStatus: latestCookieImportStatus)
await self.applyOpenAIDashboardFailure(
message: message,
expectedGuard: context.expectedGuard,
refreshTaskToken: context.refreshTaskToken,
routingTargetEmail: targetEmail)
}
}
private func retryOpenAIDashboardAfterNoData(
body: String,
context: OpenAIDashboardRefreshContext,
latestCookieImportStatus: inout String?,
logger: @escaping (String) -> Void) async
{
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
allowCurrentSnapshotFallback: context.allowCurrentSnapshotFallback,
allowLastKnownLiveFallback: context.expectedGuard?.identity != .unresolved)
var effectiveEmail = targetEmail
let imported = await self.importOpenAIDashboardCookiesIfNeeded(targetEmail: targetEmail, force: true)
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
if await self.abortOpenAIDashboardRetryAfterImportFailure(
importedEmail: imported,
targetEmail: targetEmail,
expectedGuard: context.expectedGuard,
cookieImportStatus: latestCookieImportStatus,
refreshTaskToken: context.refreshTaskToken)
{
return
}
if let imported {
effectiveEmail = imported
}
do {
let dash = try await self.loadLatestOpenAIDashboard(
accountEmail: effectiveEmail,
logger: logger,
allowNavigationTimeoutRetry: context.force,
timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true))
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
await self.applyOpenAIDashboard(
dash,
targetEmail: effectiveEmail,
expectedGuard: context.expectedGuard,
refreshTaskToken: context.refreshTaskToken,
allowCodexUsageBackfill: context.allowCodexUsageBackfill)
} catch let OpenAIDashboardFetcher.FetchError.noDashboardData(retryBody) {
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
let finalBody = retryBody.isEmpty ? body : retryBody
let message = self.openAIDashboardFriendlyError(
body: finalBody,
targetEmail: targetEmail,
cookieImportStatus: latestCookieImportStatus)
?? OpenAIDashboardFetcher.FetchError.noDashboardData(body: finalBody).localizedDescription
await self.applyOpenAIDashboardFailure(
message: message,
expectedGuard: context.expectedGuard,
refreshTaskToken: context.refreshTaskToken,
routingTargetEmail: targetEmail)
} catch {
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
let message = self.preferredOpenAIDashboardFailureMessage(
error: error,
targetEmail: targetEmail,
cookieImportStatus: latestCookieImportStatus)
await self.applyOpenAIDashboardFailure(
message: message,
expectedGuard: context.expectedGuard,
refreshTaskToken: context.refreshTaskToken,
routingTargetEmail: targetEmail)
}
}
private func retryOpenAIDashboardAfterLoginRequired(
context: OpenAIDashboardRefreshContext,
latestCookieImportStatus: inout String?,
logger: @escaping (String) -> Void) async
{
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
allowCurrentSnapshotFallback: context.allowCurrentSnapshotFallback,
allowLastKnownLiveFallback: context.expectedGuard?.identity != .unresolved)
var effectiveEmail = targetEmail
let imported = await self.importOpenAIDashboardCookiesIfNeeded(targetEmail: targetEmail, force: true)
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
if await self.abortOpenAIDashboardRetryAfterImportFailure(
importedEmail: imported,
targetEmail: targetEmail,
expectedGuard: context.expectedGuard,
cookieImportStatus: latestCookieImportStatus,
refreshTaskToken: context.refreshTaskToken)
{
return
}
if let imported {
effectiveEmail = imported
}
do {
let dash = try await self.loadLatestOpenAIDashboard(
accountEmail: effectiveEmail,
logger: logger,
allowNavigationTimeoutRetry: context.force,
timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true))
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
await self.applyOpenAIDashboard(
dash,
targetEmail: effectiveEmail,
expectedGuard: context.expectedGuard,
refreshTaskToken: context.refreshTaskToken,
allowCodexUsageBackfill: context.allowCodexUsageBackfill)
} catch OpenAIDashboardFetcher.FetchError.loginRequired {
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
await self.applyOpenAIDashboardLoginRequiredFailure(
expectedGuard: context.expectedGuard,
refreshTaskToken: context.refreshTaskToken,
routingTargetEmail: targetEmail)
} catch {
guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
let message = self.preferredOpenAIDashboardFailureMessage(
error: error,
targetEmail: targetEmail,
cookieImportStatus: latestCookieImportStatus)
await self.applyOpenAIDashboardFailure(
message: message,
expectedGuard: context.expectedGuard,
refreshTaskToken: context.refreshTaskToken,
routingTargetEmail: targetEmail)
}
}
// MARK: - OpenAI web account switching
/// Detect Codex account-source changes and clear stale OpenAI web state so the UI can't show the wrong user.
/// This does not delete other isolated WebKit cookie stores (we keep multiple accounts around).
func handleOpenAIWebTargetEmailChangeIfNeeded(
targetEmail: String?,
targetScope: CookieHeaderCache.Scope? = nil)
{
let normalized = targetEmail?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
guard let normalized, !normalized.isEmpty else { return }
let isolationKey = Self.openAIWebTargetIsolationKey(email: normalized, scope: targetScope)
let previousIsolationKey = self.lastOpenAIDashboardTargetIsolationKey
self.lastOpenAIDashboardTargetEmail = normalized
self.lastOpenAIDashboardTargetIsolationKey = isolationKey
if let previousIsolationKey,
previousIsolationKey != isolationKey
{
let stamp = Date().formatted(date: .abbreviated, time: .shortened)
self.logOpenAIWeb(
"[\(stamp)] Codex account source changed; clearing OpenAI web snapshot")
self.openAIWebAccountDidChange = true
self.openAIDashboard = nil
self.openAIDashboardAttachmentAuthorized = false
self.lastOpenAIDashboardSnapshot = nil
self.lastOpenAIDashboardAttachmentAuthorized = false
self.lastOpenAIDashboardError = nil
self.lastOpenAIDashboardAttemptAt = nil
self.openAIDashboardRequiresLogin = true
self.openAIDashboardCookieImportStatus = L("Codex account changed; importing browser cookies…")
self.lastOpenAIDashboardCookieImportAttemptAt = nil
self.lastOpenAIDashboardCookieImportEmail = nil
}
}
nonisolated static func openAIWebTargetIsolationKey(
email: String,
scope: CookieHeaderCache.Scope?) -> String
{
"\(email.lowercased())|\(scope?.isolationIdentifier ?? "live")"
}
func importOpenAIDashboardBrowserCookiesNow() async {
self.resetOpenAIWebDebugLog(context: "manual import")
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
allowCurrentSnapshotFallback: true,
allowLastKnownLiveFallback: false)
_ = await self.importOpenAIDashboardCookiesIfNeeded(targetEmail: targetEmail, force: true)
let expectedGuard = self.freshCodexOpenAIWebRefreshGuard()
await self.refreshOpenAIDashboardIfNeeded(
force: true,
expectedGuard: expectedGuard,
bypassCoalescing: true)
}
func currentCodexOpenAIWebTargetEmail(
allowCurrentSnapshotFallback: Bool,
allowLastKnownLiveFallback: Bool) -> String?
{
switch self.settings.codexResolvedActiveSource {
case .liveSystem:
let liveSystem = self.settings.codexAccountReconciliationSnapshot.liveSystemAccount?.email
.trimmingCharacters(in: .whitespacesAndNewlines)
if let liveSystem, !liveSystem.isEmpty {
self.lastKnownLiveSystemCodexEmail = liveSystem
return liveSystem
}
if allowCurrentSnapshotFallback,
let snapshotEmail = self.snapshots[.codex]?.accountEmail(for: .codex)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!snapshotEmail.isEmpty
{
self.lastKnownLiveSystemCodexEmail = snapshotEmail
return snapshotEmail
}
if allowLastKnownLiveFallback {
let lastKnown = self.lastKnownLiveSystemCodexEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
if let lastKnown, !lastKnown.isEmpty {
return lastKnown
}
}
return nil
case .managedAccount:
return self.codexAccountEmailForOpenAIDashboard()
case let .profileHome(path):
return self.currentProfileCodexRuntimeEmail(path: path)
}
}
private func openAIDashboardRefreshKey(
targetEmail: String?,
expectedGuard: CodexAccountScopedRefreshGuard?) -> String
{
let source = String(describing: expectedGuard?.source ?? self.settings.codexResolvedActiveSource)
let identityKey = Self.codexIdentityGuardKey(expectedGuard?.identity ?? .unresolved) ?? "unresolved"
let accountKey = Self.normalizeCodexAccountScopedKey(targetEmail) ?? "unknown"
let authFingerprint = CodexAuthFingerprint.normalize(expectedGuard?.authFingerprint) ?? "nil"
return "\(source)|\(identityKey)|\(accountKey)|auth:\(authFingerprint)"
}
private func actionableOpenAIDashboardImportFailure(targetEmail: String?) -> String? {
self.actionableOpenAIDashboardImportFailure(
targetEmail: targetEmail,
cookieImportStatus: self.openAIDashboardCookieImportStatus)
}
private func actionableOpenAIDashboardImportFailure(
targetEmail: String?,
cookieImportStatus: String?) -> String?
{
let status = cookieImportStatus?.trimmingCharacters(in: .whitespacesAndNewlines)
guard let status, !status.isEmpty else { return nil }
if status.localizedCaseInsensitiveContains("openai cookies are for") {
return "\(status) Switch chatgpt.com account, then refresh OpenAI cookies."
}
if status.localizedCaseInsensitiveContains("no signed-in openai web session found")
|| status.localizedCaseInsensitiveContains("no matching openai web session found")
{
let targetLabel = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
let accountLabel = (targetLabel?.isEmpty == false) ? targetLabel! : "your OpenAI account"
return "\(status) Sign in to chatgpt.com as \(accountLabel), then refresh OpenAI cookies."
}
if status.localizedCaseInsensitiveContains("openai cookie import failed")
|| status.localizedCaseInsensitiveContains("browser cookie import failed")
{
return status
}
return nil
}
private func preferredOpenAIDashboardFailureMessage(
error: Error,
targetEmail: String?,
cookieImportStatus: String?) -> String
{
if let actionable = self.actionableOpenAIDashboardImportFailure(
targetEmail: targetEmail,
cookieImportStatus: cookieImportStatus)
{
return actionable
}
return error.localizedDescription
}
private static func isOpenAIDashboardTimeout(_ error: Error) -> Bool {
let nsError = error as NSError
return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorTimedOut
}
private func abortOpenAIDashboardRetryAfterImportFailure(
importedEmail: String?,
targetEmail: String?,
expectedGuard: CodexAccountScopedRefreshGuard?,
cookieImportStatus: String?,
refreshTaskToken: UUID) async -> Bool
{
guard importedEmail == nil,
let message = self.actionableOpenAIDashboardImportFailure(
targetEmail: targetEmail,
cookieImportStatus: cookieImportStatus)
else {
return false
}
await self.applyOpenAIDashboardFailure(
message: message,
expectedGuard: expectedGuard,
refreshTaskToken: refreshTaskToken,
routingTargetEmail: targetEmail)
return true
}
private func shouldApplyOpenAIDashboardRefreshTask(token: UUID?) -> Bool {
guard let token else { return true }
return self.openAIDashboardRefreshTaskToken == token
}
private func shouldContinueOpenAIDashboardRefresh(token: UUID?) -> Bool {
!Task.isCancelled && self.shouldApplyOpenAIDashboardRefreshTask(token: token)
}
func invalidateOpenAIDashboardRefreshTask() {
self.openAIDashboardBackgroundRefreshTask?.cancel()
self.openAIDashboardBackgroundRefreshTask = nil
self.openAIDashboardBackgroundRefreshTaskKey = nil
self.openAIDashboardRefreshTask?.cancel()
self.openAIDashboardRefreshTask = nil
self.openAIDashboardRefreshTaskKey = nil
self.openAIDashboardRefreshTaskToken = nil
}
private func currentOpenAIDashboardCookieImportStatus() -> String? {
self.openAIDashboardCookieImportStatus
}
private func loadLatestOpenAIDashboard(
accountEmail: String?,
logger: @escaping (String) -> Void,
allowNavigationTimeoutRetry: Bool,
timeout: TimeInterval) async throws -> OpenAIDashboardSnapshot
{
if let override = self._test_openAIDashboardLoaderOverride {
return try await override(accountEmail, logger, allowNavigationTimeoutRetry, timeout)
}
return try await OpenAIDashboardFetcher().loadLatestDashboard(
accountEmail: accountEmail,
cacheScope: self.codexCookieCacheScopeForOpenAIWeb(),
logger: logger,
debugDumpHTML: timeout != Self.openAIWebPrimaryFetchTimeout,
allowNavigationTimeoutRetry: allowNavigationTimeoutRetry,
timeout: timeout)
}
private func failClosedForUnreadableManagedCodexStore() async -> String? {
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
self.openAIDashboardRequiresLogin = true
self.openAIDashboardCookieImportStatus = [
L("Managed Codex account data is unavailable."),
L("Fix the managed account store before importing OpenAI cookies."),
].joined(separator: " ")
return nil
}
private func failClosedRefreshForUnreadableManagedCodexStore() async {
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
self.openAIDashboardRequiresLogin = true
self.lastOpenAIDashboardError = [
L("Managed Codex account data is unavailable."),
L("Fix the managed account store before refreshing OpenAI web data."),
].joined(separator: " ")
}
private func failClosedForMissingManagedCodexTarget() async -> String? {
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
self.openAIDashboardRequiresLogin = true
self.openAIDashboardCookieImportStatus = [
L("The selected managed Codex account is unavailable."),
L("Pick another Codex account before importing OpenAI cookies."),
].joined(separator: " ")
return nil
}
private func failClosedRefreshForMissingManagedCodexTarget() async {
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
self.openAIDashboardRequiresLogin = true
self.lastOpenAIDashboardError = [
L("The selected managed Codex account is unavailable."),
L("Pick another Codex account before refreshing OpenAI web data."),
].joined(separator: " ")
}
private func failClosedForMissingProfileCodexTarget() async -> String? {
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
self.openAIDashboardRequiresLogin = true
self.openAIDashboardCookieImportStatus = [
L("The selected Codex profile has no verified account email."),
L("Refresh the profile before importing OpenAI cookies."),
].joined(separator: " ")
return nil
}
private func failClosedRefreshForMissingProfileCodexTarget() async {
self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
self.openAIDashboardRequiresLogin = true
self.lastOpenAIDashboardError = [
L("The selected Codex profile has no verified account email."),
L("Refresh the profile before refreshing OpenAI web data."),
].joined(separator: " ")
}
private func openAIWebCookieImportShouldFailClosed() async -> Bool {
if self.openAIWebManagedTargetStoreIsUnreadable() {
_ = await self.failClosedForUnreadableManagedCodexStore()
return true
}
if self.openAIWebManagedTargetIsMissing() {
_ = await self.failClosedForMissingManagedCodexTarget()
return true
}
if self.openAIWebProfileTargetEmailIsMissing() {
_ = await self.failClosedForMissingProfileCodexTarget()
return true
}
return false
}
private func openAIDashboardCookieImportResult(
request: OpenAIDashboardCookieImportRequest,
logger: @escaping (String) -> Void) async throws -> OpenAIDashboardBrowserCookieImporter.ImportResult
{
if let override = self._test_openAIDashboardCookieImportOverride {
return try await override(
request.normalizedTarget,
request.allowAnyAccount,
request.cookieSource,
request.cacheScope,
logger)
}
let importer = OpenAIDashboardBrowserCookieImporter(browserDetection: self.browserDetection)
switch request.cookieSource {
case .manual:
self.settings.ensureCodexCookieLoaded()
// Manual OpenAI cookies still come from one provider-level setting. Auto-imported cookies are
// isolated per managed account, but a manual header is an explicit override owned by settings,
// so switching managed accounts does not currently swap it underneath the user.
let manualHeader = self.settings.codexCookieHeader
guard CookieHeaderNormalizer.normalize(manualHeader) != nil else {
throw OpenAIDashboardBrowserCookieImporter.ImportError.manualCookieHeaderInvalid
}
return try await importer.importManualCookies(
cookieHeader: manualHeader,
intoAccountEmail: request.normalizedTarget,
allowAnyAccount: request.allowAnyAccount,
cacheScope: request.cacheScope,
logger: logger)
case .auto:
return try await importer.importBestCookies(
intoAccountEmail: request.normalizedTarget,
allowAnyAccount: request.allowAnyAccount,
preferCachedCookieHeader: request.preferCachedCookieHeader ?? !request.force,
cacheScope: request.cacheScope,
logger: logger)
case .off:
return OpenAIDashboardBrowserCookieImporter.ImportResult(
sourceLabel: "Off",
cookieCount: 0,
signedInEmail: request.normalizedTarget,
matchesCodexEmail: true)
}
}
func importOpenAIDashboardCookiesIfNeeded(
targetEmail: String?,
force: Bool,
preferCachedCookieHeader: Bool? = nil) async -> String?
{
guard !Task.isCancelled else { return nil }
if await self.openAIWebCookieImportShouldFailClosed() {
return nil
}
guard !Task.isCancelled else { return nil }
let normalizedTarget = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
let allowAnyAccount = normalizedTarget == nil || normalizedTarget?.isEmpty == true
let cookieSource = self.settings.codexCookieSource
let cacheScope = self.codexCookieCacheScopeForOpenAIWeb()
let now = Date()
let lastEmail = self.lastOpenAIDashboardCookieImportEmail
let lastAttempt = self.lastOpenAIDashboardCookieImportAttemptAt ?? .distantPast
let shouldAttempt: Bool = if force {
true
} else {
if allowAnyAccount {
now.timeIntervalSince(lastAttempt) > 300
} else {
self.openAIDashboardRequiresLogin &&
(
lastEmail?.lowercased() != normalizedTarget?.lowercased() || now
.timeIntervalSince(lastAttempt) > 300)
}
}
guard shouldAttempt else { return normalizedTarget }
self.lastOpenAIDashboardCookieImportEmail = normalizedTarget
self.lastOpenAIDashboardCookieImportAttemptAt = now
let stamp = now.formatted(date: .abbreviated, time: .shortened)
let targetLabel = normalizedTarget ?? "unknown"
self.logOpenAIWeb("[\(stamp)] import start (target=\(targetLabel))")
do {
let log: (String) -> Void = { [weak self] message in
guard let self else { return }
self.logOpenAIWeb(message)
}
let request = OpenAIDashboardCookieImportRequest(
normalizedTarget: normalizedTarget,
allowAnyAccount: allowAnyAccount,
cookieSource: cookieSource,
cacheScope: cacheScope,
preferCachedCookieHeader: preferCachedCookieHeader,
force: force)
let result = try await self.openAIDashboardCookieImportResult(
request: request,
logger: log)
guard !Task.isCancelled else { return nil }
let effectiveEmail = result.signedInEmail?
.trimmingCharacters(in: .whitespacesAndNewlines)
.isEmpty == false
? result.signedInEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
: normalizedTarget
self.lastOpenAIDashboardCookieImportEmail = effectiveEmail ?? normalizedTarget
await MainActor.run {
let signed = result.signedInEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
let matchText = result.matchesCodexEmail ? "matches Codex" : "does not match Codex"
let sourceLabel = switch cookieSource {
case .manual:
"Manual cookie header"
case .auto:
"\(result.sourceLabel) cookies"
case .off:
"OpenAI cookies disabled"
}
if let signed, !signed.isEmpty {
self.openAIDashboardCookieImportStatus =
allowAnyAccount
? [
"Using \(sourceLabel) (\(result.cookieCount)).",
"Signed in as \(signed).",
].joined(separator: " ")
: [
"Using \(sourceLabel) (\(result.cookieCount)).",
"Signed in as \(signed) (\(matchText)).",
].joined(separator: " ")
} else {
self.openAIDashboardCookieImportStatus =
"Using \(sourceLabel) (\(result.cookieCount))."
}
}
return effectiveEmail
} catch let err as OpenAIDashboardBrowserCookieImporter.ImportError {
guard !Task.isCancelled else { return nil }
switch err {
case let .noMatchingAccount(found):
let foundText: String = if found.isEmpty {
"no signed-in session detected in \(self.codexBrowserCookieOrder.loginHint)"
} else {
found
.sorted { lhs, rhs in
if lhs.sourceLabel == rhs.sourceLabel {
return lhs.email < rhs.email
}
return lhs.sourceLabel < rhs.sourceLabel
}
.map { "\($0.sourceLabel): \($0.email)" }
.joined(separator: " • ")
}
self.logOpenAIWeb("[\(stamp)] import mismatch: \(foundText)")
await MainActor.run {
self.openAIDashboardCookieImportStatus = allowAnyAccount
? [
"No signed-in OpenAI web session found.",
"Found \(foundText).",
].joined(separator: " ")
: Self.conciseOpenAICookieMismatchStatus(
found: found.map(\.email),
targetEmail: normalizedTarget)
self.failClosedOpenAIDashboardSnapshot()
}
case .noCookiesFound,
.browserAccessDenied,
.browserCookieLoadTimedOut,
.dashboardStillRequiresLogin,
.manualCookieHeaderInvalid:
self.logOpenAIWeb("[\(stamp)] import failed: \(err.localizedDescription)")
await MainActor.run {
self.openAIDashboardCookieImportStatus =
"OpenAI cookie import failed: \(err.localizedDescription)"
self.openAIDashboardRequiresLogin = true
}
}
} catch {
guard !Task.isCancelled else { return nil }
self.logOpenAIWeb("[\(stamp)] import failed: \(error.localizedDescription)")
await MainActor.run {
self.openAIDashboardCookieImportStatus =
"Browser cookie import failed: \(error.localizedDescription)"
}
}
return nil
}
private func resetOpenAIWebDebugLog(context: String) {
let stamp = Date().formatted(date: .abbreviated, time: .shortened)
self.openAIWebDebugLines.removeAll(keepingCapacity: true)
self.openAIDashboardCookieImportDebugLog = nil
self.logOpenAIWeb("[\(stamp)] OpenAI web \(context) start")
}
private func logOpenAIWeb(_ message: String) {
let safeMessage = LogRedactor.redact(message)
self.openAIWebLogger.debug(safeMessage)
self.openAIWebDebugLines.append(safeMessage)
if self.openAIWebDebugLines.count > 240 {
self.openAIWebDebugLines.removeFirst(self.openAIWebDebugLines.count - 240)
}
self.openAIDashboardCookieImportDebugLog = self.openAIWebDebugLines.joined(separator: "\n")
}
func resetOpenAIWebState() {
self.invalidateOpenAIDashboardRefreshTask()
OpenAIDashboardFetcher.evictAllCachedWebViews()
self.openAIDashboard = nil
self.openAIDashboardAttachmentAuthorized = false
self.lastOpenAIDashboardError = nil
self.lastOpenAIDashboardSnapshot = nil
self.lastOpenAIDashboardAttachmentAuthorized = false
self.lastOpenAIDashboardTargetEmail = nil
self.lastOpenAIDashboardTargetIsolationKey = nil
self.lastOpenAIDashboardAttemptAt = nil
self.openAIDashboardRequiresLogin = false
self.openAIDashboardCookieImportStatus = nil
self.openAIDashboardCookieImportDebugLog = nil
self.lastOpenAIDashboardCookieImportAttemptAt = nil
self.lastOpenAIDashboardCookieImportEmail = nil
self.lastKnownLiveSystemCodexEmail = nil
}
/// Routing-only optimization: this detects whether the fetched browser session appears to be for a
/// different account than the route target, so we can retry after cookie import. Ownership proof
/// happens exclusively through CodexDashboardAuthority.
private func dashboardEmailMismatch(expected: String?, actual: String?) -> Bool {
guard let expected, !expected.isEmpty else { return false }
guard let raw = actual?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return false }
return raw.lowercased() != expected.lowercased()
}
private func openAIWebManagedTargetStoreIsUnreadable() -> Bool {
guard case .managedAccount = self.settings.codexResolvedActiveSource else {
return false
}
return self.settings.codexSettingsSnapshot(tokenOverride: nil).managedAccountStoreUnreadable
}
private func openAIWebManagedTargetIsMissing() -> Bool {
guard case .managedAccount = self.settings.codexResolvedActiveSource else {
return false
}
return self.selectedManagedCodexAccountForOpenAIWeb() == nil
}
private func openAIWebProfileTargetEmailIsMissing() -> Bool {
guard case let .profileHome(path) = self.settings.codexResolvedActiveSource else {
return false
}
return self.currentProfileCodexRuntimeEmail(path: path) == nil
}
private func selectedManagedCodexAccountForOpenAIWeb() -> ManagedCodexAccount? {
guard case let .managedAccount(id) = self.settings.codexResolvedActiveSource else {
return nil
}
let snapshot = self.settings.codexAccountReconciliationSnapshot
return snapshot.storedAccounts.first { $0.id == id }
}
func codexAccountEmailForOpenAIDashboard(allowLastKnownLiveFallback: Bool = true) -> String? {
switch self.settings.codexResolvedActiveSource {
case .liveSystem:
let liveSystem = self.settings.codexAccountReconciliationSnapshot.liveSystemAccount?.email
.trimmingCharacters(in: .whitespacesAndNewlines)
if let liveSystem, !liveSystem.isEmpty {
self.lastKnownLiveSystemCodexEmail = liveSystem
return liveSystem
}
guard allowLastKnownLiveFallback else { return nil }
let lastKnown = self.lastKnownLiveSystemCodexEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
if let lastKnown, !lastKnown.isEmpty {
return lastKnown
}
return nil
case .managedAccount:
if self.openAIWebManagedTargetStoreIsUnreadable() {
return nil
}
let managed = self.currentManagedCodexRuntimeEmail()?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let managed, !managed.isEmpty {
return managed
}
return nil
case let .profileHome(path):
let profile = self.currentProfileCodexRuntimeEmail(path: path)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let profile, !profile.isEmpty {
return profile
}
return nil
}
}
func codexCookieCacheScopeForOpenAIWeb() -> CookieHeaderCache.Scope? {
switch self.settings.codexResolvedActiveSource {
case .liveSystem:
nil
case let .managedAccount(id):
self.openAIWebManagedTargetStoreIsUnreadable() ? .managedStoreUnreadable : .managedAccount(id)
case let .profileHome(path):
.profileHome(path)
}
}
}
// MARK: - OpenAI web error messaging
extension UsageStore {
nonisolated static func shouldRunOpenAIWebRefresh(_ context: OpenAIWebRefreshPolicyContext) -> Bool {
guard context.accessEnabled else { return false }
guard context.force || context.refreshPhase != .startup else { return false }
return context.force || !context.batterySaverEnabled
}
nonisolated static func forceOpenAIWebRefreshForStaleRequest(batterySaverEnabled: Bool) -> Bool {
!batterySaverEnabled
}
nonisolated static func shouldSkipOpenAIWebRefresh(_ context: OpenAIWebRefreshGateContext) -> Bool {
if context.force || context.accountDidChange {
return false
}
if let lastAttemptAt = context.lastAttemptAt,
context.now.timeIntervalSince(lastAttemptAt) < context.refreshInterval
{
return true
}
if context.lastError == nil,
let lastSnapshotAt = context.lastSnapshotAt,
context.now.timeIntervalSince(lastSnapshotAt) < context.refreshInterval
{
return true
}
return false
}
nonisolated static func shouldSkipOpenAIWebEmptyHistoryRetry(_ context: OpenAIWebRefreshGateContext) -> Bool {
if context.force || context.accountDidChange {
return false
}
guard let lastAttemptAt = context.lastAttemptAt,
context.now.timeIntervalSince(lastAttemptAt) < context.refreshInterval
else { return false }
guard let lastSnapshotAt = context.lastSnapshotAt else { return true }
return lastAttemptAt >= lastSnapshotAt
}
func syncOpenAIWebState() {
guard self.isEnabled(.codex),
self.settings.openAIWebAccessEnabled,
self.settings.codexCookieSource.isEnabled
else {
self.resetOpenAIWebState()
return
}
let targetEmail = self.currentCodexOpenAIWebTargetEmail(
allowCurrentSnapshotFallback: true,
allowLastKnownLiveFallback: true)
self.handleOpenAIWebTargetEmailChangeIfNeeded(
targetEmail: targetEmail,
targetScope: self.codexCookieCacheScopeForOpenAIWeb())
}
func openAIDashboardFriendlyError(
body: String,
targetEmail: String?,
cookieImportStatus: String?) -> String?
{
let trimmed = body.trimmingCharacters(in: .whitespacesAndNewlines)
let status = cookieImportStatus?.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
return [
"OpenAI web dashboard returned an empty page.",
"Sign in to chatgpt.com and update OpenAI cookies in Providers → Codex.",
].joined(separator: " ")
}
let lower = trimmed.lowercased()
let looksLikePublicLanding = lower.contains("skip to content")
&& (lower.contains("about") || lower.contains("openai") || lower.contains("chatgpt"))
let looksLoggedOut = lower.contains("sign in")
|| lower.contains("log in")
|| lower.contains("create account")
|| lower.contains("continue with google")
|| lower.contains("continue with apple")
|| lower.contains("continue with microsoft")
guard looksLikePublicLanding || looksLoggedOut else { return nil }
let emailLabel = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
let targetLabel = (emailLabel?.isEmpty == false) ? emailLabel! : "your OpenAI account"
if let status, !status.isEmpty {
if status.contains("cookies do not match Codex account")
|| status.localizedCaseInsensitiveContains("openai cookies are for")
|| status.localizedCaseInsensitiveContains("cookie import failed")
{
return "\(status) Switch chatgpt.com account, then refresh OpenAI cookies."
}
}
return [
"OpenAI web dashboard returned a public page (not signed in).",
"Sign in to chatgpt.com as \(targetLabel), then update OpenAI cookies in Providers → Codex.",
].joined(separator: " ")
}
private static func conciseOpenAICookieMismatchStatus(
found: [String],
targetEmail: String?)
-> String
{
let normalizedFound = Array(Set(
found
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
.filter { !$0.isEmpty }))
.sorted()
let foundLabel: String = switch normalizedFound.count {
case 0:
""
case 1:
normalizedFound[0]
case 2:
"\(normalizedFound[0]) or \(normalizedFound[1])"
default:
"\(normalizedFound[0]) or \(normalizedFound.count - 1) other accounts"
}
let targetLabel = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
if normalizedFound.isEmpty {
guard let targetLabel, !targetLabel.isEmpty else {
return L("No matching OpenAI web session found.")
}
return L("No matching OpenAI web session found for %@.", targetLabel)
}
guard let targetLabel, !targetLabel.isEmpty else {
return L("OpenAI cookies are for %@.", foundLabel)
}
return L("OpenAI cookies are for %1$@, not %2$@.", foundLabel, targetLabel)
}
}