diff --git a/CHANGELOG.md b/CHANGELOG.md index 862a68323b..1cdbb02424 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Grok: add an Auto / Grok CLI / SuperGrok OAuth / Browser cookies source picker, support pasted SuperGrok bearers and grok.com cookies in token accounts, and open `~/.grok/auth.json` from Open token file (#3010). Thanks @oakimov! ### Fixed +- Codex: classify app-server request timeouts before terminating the subprocess, so timeouts no longer surface as misleading EOF/malformed-response errors (#3022). Thanks @Chipagosfinest! - OpenCode Go: surface expired selected session tokens instead of silently replacing failed server usage with local quota estimates (#2993). Thanks @Niclassslua! - Claude spend: price bare first-party model IDs from their models.dev vendor catalog, preserve explicit routes, and leave ambiguous cross-vendor matches unpriced (#3002). Thanks @Yuxin-Qiao! - Menu: prevent the system menu highlight from painting behind provider detail cards on macOS 27 beta (#2998). Thanks @Tan1103! diff --git a/Sources/CodexBarCore/UsageFetcher.swift b/Sources/CodexBarCore/UsageFetcher.swift index e5f98b69bc..0f7b10217a 100644 --- a/Sources/CodexBarCore/UsageFetcher.swift +++ b/Sources/CodexBarCore/UsageFetcher.swift @@ -831,6 +831,11 @@ enum RPCWireError: Error, LocalizedError { } } +private enum RPCRequestRaceResult: Sendable { + case value(Value) + case timedOut +} + /// RPC helper used on background tasks; safe because we confine it to the owning task. private final class CodexRPCClient: @unchecked Sendable { // Provider-specific by design: Codex RPC owns its dedicated subprocess log category. @@ -1007,24 +1012,29 @@ private final class CodexRPCClient: @unchecked Sendable { method: String, body: @escaping @Sendable () async throws -> T) async throws -> T { - try await withThrowingTaskGroup(of: T.self) { group in + try await withThrowingTaskGroup(of: RPCRequestRaceResult.self) { group in group.addTask { - try await body() + try await .value(body()) } - group.addTask { [weak self] in + group.addTask { try await Task.sleep(for: .seconds(seconds)) - self?.terminateProcessForTimeout(method: method) - throw RPCWireError.timeout(method: method) + return .timedOut } - do { - guard let result = try await group.next() else { - throw RPCWireError.timeout(method: method) - } - group.cancelAll() - return result - } catch { + + guard let result = try await group.next() else { group.cancelAll() - throw error + throw RPCWireError.timeout(method: method) + } + group.cancelAll() + + switch result { + case let .value(value): + return value + case .timedOut: + // Terminating the process closes stdout. Classify that expected EOF as a + // timeout by selecting the timer before requesting process termination. + self.terminateProcessForTimeout(method: method) + throw RPCWireError.timeout(method: method) } } } diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 895c6aacaa..3f5ed2939f 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -1503,7 +1503,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This tagged diagnostic payload encodes MiniMax details under the matching wire key."), SuppressedProviderReference( path: "Sources/CodexBarCore/UsageFetcher.swift", - line: 1480, + line: 1490, anchor: "providerID: .codex,", expectedProviderIDs: ["codex"], reason: "This provider-specific core branch passes its already-selected identity to a shared helper."),