Files
codex/codex-rs/app-server-client/src/cloud_backend/auth.rs
T
Benjamin Carlsson f988ada4bf Reconnect idle Cloud conversations after transient stream loss (#60969)
## Why

A lost Cloud reply stream previously required reopening the conversation, even when it was idle. Recover the displayed conversation automatically while holding queued input until its reply stream is usable.

## What changed

- Add bounded retries for transient stream failures in the displayed, idle conversation, with credential and network-policy checks. Keep explicit reopen behavior when recovery is unavailable or fails.
- Enable HTTP/2 keepalive pings for Cloud connections.
- Queue input while disconnected and resume ordinary submission after recovery. Mark recovered history as potentially incomplete so an explicit reopen reloads missed turns while preserving drafts and reconciling pending submissions.
- Store retained input per backend and apply background settings updates to the owning backend's drafts.

## Testing

Add regression coverage for queued input during disconnection and extend resume and backend-handoff tests to cover history refresh, draft preservation, and retained settings.

GitOrigin-RevId: 3d753e53d3c0d594680a90cac8a75fe33b91e219
2026-11-12 01:57:33 +00:00

63 lines
2.4 KiB
Rust

//! Opens the internal Cloud preview with the selected ChatGPT credentials and local transport policy.
use super::start_worker;
use crate::AppServerClient;
use crate::legacy_core::config::Config;
use codex_app_server_protocol as protocol;
use codex_cloud_client as cloud;
use codex_login::token_data::parse_jwt_expiration;
use codex_protocol::account::PlanType;
use color_eyre::eyre::Result;
use color_eyre::eyre::bail;
use color_eyre::eyre::eyre;
const ORIGIN: &str = "https://codex-cloud-backend.chatgpt.com";
pub async fn connect(config: &Config) -> Result<AppServerClient> {
// The preview borrows Local's policy without owning credential updates.
let factory = config.http_client_factory();
let policy = factory.network_policy().clone().for_current_account();
let factory = factory.with_network_policy(policy);
let storage = config.auth_config();
let auth = storage
.load_auth(/*enable_codex_api_key_env*/ false)
.await?
.ok_or_else(|| eyre!("Sign in with ChatGPT and reopen Codex to use Cloud."))?;
if !auth.is_chatgpt_auth() || auth.is_fedramp_account() {
bail!("This account is not supported by the cloud conversation preview.");
}
let account_id = auth
.get_account_id()
.ok_or_else(|| eyre!("Cloud requires a selected account."))?;
let token = auth.get_token()?;
if let Ok(Some(expires)) = parse_jwt_expiration(&token)
&& std::time::SystemTime::from(expires) <= std::time::SystemTime::now()
{
bail!("Your ChatGPT credential expired. Reopen Codex to reconnect.");
}
let origin = ORIGIN.parse()?;
let permit = factory.network_policy().acquire(&origin)?;
let credentials = cloud::Credentials::new(&token, &account_id)?;
let client = cloud::Client::new(&factory, ORIGIN, credentials)?;
Ok(start_worker(
client,
super::idle::Login {
storage,
mode: auth.api_auth_mode(),
token,
account: account_id,
permit,
},
protocol::GetAccountResponse {
user_id: auth.get_chatgpt_user_id(),
account: Some(protocol::Account::Chatgpt {
email: auth.get_account_email(),
plan_type: auth.account_plan_type().unwrap_or(PlanType::Unknown),
workspace_name: None,
}),
requires_openai_auth: true,
workspace_routing: None,
},
))
}