import CodexBarCoreimport 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} // MARK: - OpenAI web lifecycle extension UsageStore { private struct OpenAIDashboardRefreshContext { let targetEmail: String? let allowCurrentSnapshotFallback: Bool let expectedGuard: CodexAccountScopedRefreshGuard? let refreshTaskToken: UUID let allowCodexUsageBackfill: Bool } private static let openAIWebRefreshMultiplier: TimeInterval = 5 private static let openAIWebPrimaryFetchTimeout: TimeInterval = 15 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 } private func openAIWebRefreshIntervalSeconds() -> TimeInterval { let base = max(self.settings.refreshFrequency.seconds ?? 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 lastUpdatedAt = self.openAIDashboard?.updatedAt ?? self.lastOpenAIDashboardSnapshot?.updatedAt if let lastUpdatedAt, now.timeIntervalSince(lastUpdatedAt) < refreshInterval { 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) self.openAIWebLogger.debug( "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.currentCodexOpenAIWebRefreshGuard() 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 } if let expectedGuard, !self.shouldApplyOpenAIDashboardRefreshGuard( expectedGuard: expectedGuard, routingTargetEmail: targetEmail) { return } let authority = self.evaluateCodexDashboardAuthority( dashboard: dash, sourceKind: .liveWeb, routingTargetEmail: targetEmail) let attachedAccountEmail = self.codexDashboardAttachmentEmail(from: authority.input) 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 } 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 } 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) { self.snapshots[.codex] = usage self.errors[.codex] = nil self.failureGates[.codex]?.recordSuccess() self.lastSourceLabels[.codex] = "openai-web" } 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.snapshots.removeValue(forKey: .codex) self.errors[.codex] = nil self.lastSourceLabels.removeValue(forKey: .codex) self.lastFetchAttempts.removeValue(forKey: .codex) self.accountSnapshots.removeValue(forKey: .codex) self.failureGates[.codex]?.reset() self.lastKnownSessionRemaining.removeValue(forKey: .codex) self.lastKnownSessionWindowSource.removeValue(forKey: .codex) } 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() { self.lastCodexAccountScopedRefreshGuard = self.currentCodexAccountScopedRefreshGuard( preferCurrentSnapshot: false, allowLastKnownLiveFallback: false) } 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 } 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 } self.handleOpenAIWebTargetEmailChangeIfNeeded(targetEmail: targetEmail) 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) 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 } } private func performOpenAIDashboardRefreshIfNeeded(_ context: OpenAIDashboardRefreshContext) async { 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) 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, timeout: Self.openAIWebDashboardFetchTimeout(didImportCookies: didImportCookiesForRefresh)) if self.dashboardEmailMismatch(expected: normalized, actual: dash.signedInEmail) { if let imported = await self.importOpenAIDashboardCookiesIfNeeded( targetEmail: context.targetEmail, force: true) { effectiveEmail = imported } latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus() dash = try await self.loadLatestOpenAIDashboard( accountEmail: effectiveEmail, logger: log, timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true)) } await self.applyOpenAIDashboard( dash, targetEmail: effectiveEmail, expectedGuard: context.expectedGuard, refreshTaskToken: context.refreshTaskToken, allowCodexUsageBackfill: context.allowCodexUsageBackfill) } catch let OpenAIDashboardFetcher.FetchError.noDashboardData(body) { await self.retryOpenAIDashboardAfterNoData( body: body, context: context, latestCookieImportStatus: &latestCookieImportStatus, logger: log) } catch OpenAIDashboardFetcher.FetchError.loginRequired { await self.retryOpenAIDashboardAfterLoginRequired( context: context, latestCookieImportStatus: &latestCookieImportStatus, logger: log) } catch { let message = self.preferredOpenAIDashboardFailureMessage( error: error, targetEmail: context.targetEmail, cookieImportStatus: latestCookieImportStatus)Lines 1–500 of 1230. Load more
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 } // MARK: - OpenAI web lifecycle extension UsageStore { private struct OpenAIDashboardRefreshContext { let targetEmail: String? let allowCurrentSnapshotFallback: Bool let expectedGuard: CodexAccountScopedRefreshGuard? let refreshTaskToken: UUID let allowCodexUsageBackfill: Bool } private static let openAIWebRefreshMultiplier: TimeInterval = 5 private static let openAIWebPrimaryFetchTimeout: TimeInterval = 15 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 } private func openAIWebRefreshIntervalSeconds() -> TimeInterval { let base = max(self.settings.refreshFrequency.seconds ?? 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 lastUpdatedAt = self.openAIDashboard?.updatedAt ?? self.lastOpenAIDashboardSnapshot?.updatedAt if let lastUpdatedAt, now.timeIntervalSince(lastUpdatedAt) < refreshInterval { 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) self.openAIWebLogger.debug( "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.currentCodexOpenAIWebRefreshGuard() 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 } if let expectedGuard, !self.shouldApplyOpenAIDashboardRefreshGuard( expectedGuard: expectedGuard, routingTargetEmail: targetEmail) { return } let authority = self.evaluateCodexDashboardAuthority( dashboard: dash, sourceKind: .liveWeb, routingTargetEmail: targetEmail) let attachedAccountEmail = self.codexDashboardAttachmentEmail(from: authority.input) 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 } 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 } 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) { self.snapshots[.codex] = usage self.errors[.codex] = nil self.failureGates[.codex]?.recordSuccess() self.lastSourceLabels[.codex] = "openai-web" } 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.snapshots.removeValue(forKey: .codex) self.errors[.codex] = nil self.lastSourceLabels.removeValue(forKey: .codex) self.lastFetchAttempts.removeValue(forKey: .codex) self.accountSnapshots.removeValue(forKey: .codex) self.failureGates[.codex]?.reset() self.lastKnownSessionRemaining.removeValue(forKey: .codex) self.lastKnownSessionWindowSource.removeValue(forKey: .codex) } 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() { self.lastCodexAccountScopedRefreshGuard = self.currentCodexAccountScopedRefreshGuard( preferCurrentSnapshot: false, allowLastKnownLiveFallback: false) } 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 } 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 } self.handleOpenAIWebTargetEmailChangeIfNeeded(targetEmail: targetEmail) 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) 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 } } private func performOpenAIDashboardRefreshIfNeeded(_ context: OpenAIDashboardRefreshContext) async { 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) 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, timeout: Self.openAIWebDashboardFetchTimeout(didImportCookies: didImportCookiesForRefresh)) if self.dashboardEmailMismatch(expected: normalized, actual: dash.signedInEmail) { if let imported = await self.importOpenAIDashboardCookiesIfNeeded( targetEmail: context.targetEmail, force: true) { effectiveEmail = imported } latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus() dash = try await self.loadLatestOpenAIDashboard( accountEmail: effectiveEmail, logger: log, timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true)) } await self.applyOpenAIDashboard( dash, targetEmail: effectiveEmail, expectedGuard: context.expectedGuard, refreshTaskToken: context.refreshTaskToken, allowCodexUsageBackfill: context.allowCodexUsageBackfill) } catch let OpenAIDashboardFetcher.FetchError.noDashboardData(body) { await self.retryOpenAIDashboardAfterNoData( body: body, context: context, latestCookieImportStatus: &latestCookieImportStatus, logger: log) } catch OpenAIDashboardFetcher.FetchError.loginRequired { await self.retryOpenAIDashboardAfterLoginRequired( context: context, latestCookieImportStatus: &latestCookieImportStatus, logger: log) } catch { let message = self.preferredOpenAIDashboardFailureMessage( error: error, targetEmail: context.targetEmail, cookieImportStatus: latestCookieImportStatus)