Skip to content

Commit 8ee2fbc

Browse files
committed
feat: reapply fork behavior on rust-v0.149.0
Re-implement the codext fork behavior on top of upstream rust-v0.149.0, adapting to upstream refactors in app-server request processors, the TUI app/chatwidget modules, and the login auth manager. - config: [tui] usage_limit_resume_prompt and server_overloaded_resume - login: AuthReloadStatus/reload_with_status, storage-based reloads, refresh-relevant auth change detection - core: model transport cache invalidation chain (client/thread/manager) - app-server: reload auth from storage when idle before thread/start, thread/resume, turn/start and account/get (reload_auth_from_storage), guarded by an auth transition lock; emit AccountUpdated and refresh cloud requirements after a changed reload - tui: status header (model/dir/git/rate-limit/account) with background git-status poller, 15s rate-limit polling, auth.json watcher with deferred reload + retries + plan-type change warnings, usage-limit recovery turn parking until account switch, queued-message autosend pause/resume on quota exhaustion/recovery, bounded server-overloaded auto-resume with backoff, Ctrl+Shift+C composer draft copy with footer hint, turn-scoped local user-message echo dedupe - naming: user-facing resume hints use `codext resume` - release CI sync: upstream CODEX_REPO_ROOT packaging env and zsh manifest verification are NOT_APPLIED (codext does not consume the codex-zsh artifact flow or the upstream assemble-codex-package path)
1 parent 60f9ff9 commit 8ee2fbc

46 files changed

Lines changed: 1617 additions & 189 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

codex-rs/Cargo.lock

Lines changed: 139 additions & 139 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/app-server-protocol/src/protocol/v2/account.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,13 @@ pub struct GetAccountParams {
513513
/// themselves and call `account/login/start` with `chatgptAuthTokens`.
514514
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
515515
pub refresh_token: bool,
516+
517+
/// When `true`, reloads the auth snapshot from storage before returning.
518+
///
519+
/// This keeps long-lived clients in sync with `auth.json` updates without
520+
/// requiring a full app-server restart.
521+
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
522+
pub reload_auth_from_storage: bool,
516523
}
517524

518525
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -521,6 +528,8 @@ pub struct GetAccountParams {
521528
pub struct GetAccountResponse {
522529
pub account: Option<Account>,
523530
pub requires_openai_auth: bool,
531+
/// Whether this request reloaded a different auth snapshot from storage.
532+
pub auth_changed: bool,
524533
}
525534

526535
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]

codex-rs/app-server/src/message_processor.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ impl MessageProcessor {
374374
let pending_thread_unloads = Arc::new(Mutex::new(HashSet::new()));
375375
let thread_watch_manager =
376376
crate::thread_status::ThreadWatchManager::new_with_outgoing(outgoing.clone());
377+
let auth_transition_lock = Arc::new(Mutex::new(()));
377378
let thread_list_state_permit = Arc::new(Semaphore::new(/*permits*/ 1));
378379
let app_list_shutdown_token = CancellationToken::new();
379380
let request_serialization_queues = RequestSerializationQueues::default();
@@ -397,6 +398,8 @@ impl MessageProcessor {
397398
outgoing.clone(),
398399
Arc::clone(&config),
399400
config_manager.clone(),
401+
thread_watch_manager.clone(),
402+
Arc::clone(&auth_transition_lock),
400403
);
401404
let apps_processor = AppsRequestProcessor::new(
402405
auth_manager.clone(),
@@ -490,6 +493,7 @@ impl MessageProcessor {
490493
Arc::clone(&pending_thread_unloads),
491494
thread_state_manager.clone(),
492495
thread_watch_manager.clone(),
496+
Arc::clone(&auth_transition_lock),
493497
Arc::clone(&thread_list_state_permit),
494498
thread_goal_processor.clone(),
495499
state_db.clone(),
@@ -509,6 +513,7 @@ impl MessageProcessor {
509513
pending_thread_unloads,
510514
thread_state_manager,
511515
thread_watch_manager,
516+
auth_transition_lock,
512517
thread_list_state_permit,
513518
Arc::clone(&skills_watcher),
514519
turn_cost_worker.as_ref().map(TurnCostWorker::handle),

codex-rs/app-server/src/request_processors.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::auth_mode::auth_mode_to_api;
12
use crate::bespoke_event_handling::apply_bespoke_event_handling;
23
use crate::command_exec::CommandExecManager;
34
use crate::command_exec::StartCommandExecParams;
@@ -402,6 +403,7 @@ use codex_feedback::FeedbackUploadOptions;
402403
use codex_git_utils::git_diff_to_remote;
403404
use codex_git_utils::resolve_root_git_project_for_trust;
404405
use codex_login::AuthManager;
406+
use codex_login::AuthReloadStatus;
405407
use codex_login::CODEX_OPEN_APP_URL;
406408
use codex_login::CodexAuth;
407409
use codex_login::LoginSuccessPage;
@@ -529,6 +531,93 @@ use uuid::Uuid;
529531
#[cfg(test)]
530532
use codex_app_server_protocol::ServerRequest;
531533

534+
/// Reload auth from storage only when no turn is currently running.
535+
///
536+
/// This keeps long-lived clients in sync with `auth.json` updates without
537+
/// hot-swapping auth in the middle of an active turn. When the reloaded
538+
/// snapshot changes, loaded threads invalidate their cached model-transport
539+
/// state so a WebSocket session created under the previous account is not
540+
/// reused for the next turn, and an `AccountUpdated` notification is emitted.
541+
async fn reload_auth_from_storage_if_idle(
542+
auth_manager: &Arc<AuthManager>,
543+
thread_manager: &Arc<ThreadManager>,
544+
config_manager: &ConfigManager,
545+
outgoing: &OutgoingMessageSender,
546+
thread_watch_manager: &ThreadWatchManager,
547+
chatgpt_base_url: &str,
548+
http_client_factory: codex_http_client::HttpClientFactory,
549+
reason: &str,
550+
) {
551+
if *thread_watch_manager.subscribe_running_turn_count().borrow() != 0 {
552+
return;
553+
}
554+
555+
let status = auth_manager.reload_with_status().await;
556+
match handle_auth_reload_status(
557+
status,
558+
auth_manager,
559+
thread_manager,
560+
config_manager,
561+
outgoing,
562+
chatgpt_base_url,
563+
http_client_factory,
564+
reason,
565+
)
566+
.await
567+
{
568+
AuthReloadStatus::Reloaded { .. } => {}
569+
AuthReloadStatus::Failed => {
570+
warn!("failed to reload auth from storage before {reason}");
571+
}
572+
}
573+
}
574+
575+
/// Apply the side effects of an unconditional auth reload and return the status.
576+
async fn handle_auth_reload_status(
577+
status: AuthReloadStatus,
578+
auth_manager: &Arc<AuthManager>,
579+
thread_manager: &Arc<ThreadManager>,
580+
config_manager: &ConfigManager,
581+
outgoing: &OutgoingMessageSender,
582+
chatgpt_base_url: &str,
583+
http_client_factory: codex_http_client::HttpClientFactory,
584+
reason: &str,
585+
) -> AuthReloadStatus {
586+
match status {
587+
AuthReloadStatus::Reloaded { changed } => {
588+
if changed {
589+
let invalidated_thread_count =
590+
thread_manager.invalidate_model_transport_caches().await;
591+
info!(
592+
"auth reloaded from storage before {reason}; invalidated model transport caches for {invalidated_thread_count} tracked thread(s)"
593+
);
594+
config_manager.replace_cloud_config_bundle_loader(
595+
Arc::clone(auth_manager),
596+
chatgpt_base_url.to_string(),
597+
http_client_factory,
598+
);
599+
config_manager
600+
.sync_default_client_residency_requirement()
601+
.await;
602+
let auth = auth_manager.auth_cached();
603+
outgoing
604+
.send_server_notification(ServerNotification::AccountUpdated(
605+
AccountUpdatedNotification {
606+
auth_mode: auth
607+
.as_ref()
608+
.map(CodexAuth::api_auth_mode)
609+
.map(auth_mode_to_api),
610+
plan_type: auth.as_ref().and_then(CodexAuth::account_plan_type),
611+
},
612+
))
613+
.await;
614+
}
615+
AuthReloadStatus::Reloaded { changed }
616+
}
617+
AuthReloadStatus::Failed => AuthReloadStatus::Failed,
618+
}
619+
}
620+
532621
mod account_processor;
533622
mod apps_processor;
534623
mod bedrock_auth;

codex-rs/app-server/src/request_processors/account_processor.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ pub(crate) struct AccountRequestProcessor {
7676
outgoing: Arc<OutgoingMessageSender>,
7777
config: Arc<Config>,
7878
config_manager: ConfigManager,
79+
thread_watch_manager: ThreadWatchManager,
80+
auth_transition_lock: Arc<Mutex<()>>,
7981
active_login: Arc<Mutex<Option<ActiveLogin>>>,
8082
}
8183

@@ -86,13 +88,17 @@ impl AccountRequestProcessor {
8688
outgoing: Arc<OutgoingMessageSender>,
8789
config: Arc<Config>,
8890
config_manager: ConfigManager,
91+
thread_watch_manager: ThreadWatchManager,
92+
auth_transition_lock: Arc<Mutex<()>>,
8993
) -> Self {
9094
Self {
9195
auth_manager,
9296
thread_manager,
9397
outgoing,
9498
config,
9599
config_manager,
100+
thread_watch_manager,
101+
auth_transition_lock,
96102
active_login: Arc::new(Mutex::new(None)),
97103
}
98104
}
@@ -1039,6 +1045,36 @@ impl AccountRequestProcessor {
10391045
params: GetAccountParams,
10401046
) -> Result<GetAccountResponse, JSONRPCErrorError> {
10411047
let do_refresh = params.refresh_token;
1048+
let mut auth_changed = false;
1049+
1050+
if params.reload_auth_from_storage {
1051+
let _auth_transition_guard = self.auth_transition_lock.lock().await;
1052+
if *self
1053+
.thread_watch_manager
1054+
.subscribe_running_turn_count()
1055+
.borrow()
1056+
== 0
1057+
{
1058+
let status = self.auth_manager.reload_with_status().await;
1059+
match handle_auth_reload_status(
1060+
status,
1061+
&self.auth_manager,
1062+
&self.thread_manager,
1063+
&self.config_manager,
1064+
&self.outgoing,
1065+
&self.config.chatgpt_base_url,
1066+
self.config.http_client_factory(),
1067+
"account/get",
1068+
)
1069+
.await
1070+
{
1071+
AuthReloadStatus::Reloaded { changed } => auth_changed = changed,
1072+
AuthReloadStatus::Failed => {
1073+
return Err(internal_error("failed to reload auth from storage"));
1074+
}
1075+
}
1076+
}
1077+
}
10421078

10431079
self.refresh_token_if_requested(do_refresh).await;
10441080

@@ -1054,6 +1090,7 @@ impl AccountRequestProcessor {
10541090
Ok(GetAccountResponse {
10551091
account,
10561092
requires_openai_auth: account_state.requires_openai_auth,
1093+
auth_changed,
10571094
})
10581095
}
10591096

codex-rs/app-server/src/request_processors/thread_processor.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ pub(crate) struct ThreadRequestProcessor {
433433
pub(super) pending_thread_unloads: Arc<Mutex<HashSet<ThreadId>>>,
434434
pub(super) thread_state_manager: ThreadStateManager,
435435
pub(super) thread_watch_manager: ThreadWatchManager,
436+
pub(super) auth_transition_lock: Arc<Mutex<()>>,
436437
pub(super) thread_list_state_permit: Arc<Semaphore>,
437438
pub(super) thread_goal_processor: ThreadGoalRequestProcessor,
438439
pub(super) state_db: Option<StateDbHandle>,
@@ -467,6 +468,7 @@ impl ThreadRequestProcessor {
467468
pending_thread_unloads: Arc<Mutex<HashSet<ThreadId>>>,
468469
thread_state_manager: ThreadStateManager,
469470
thread_watch_manager: ThreadWatchManager,
471+
auth_transition_lock: Arc<Mutex<()>>,
470472
thread_list_state_permit: Arc<Semaphore>,
471473
thread_goal_processor: ThreadGoalRequestProcessor,
472474
state_db: Option<StateDbHandle>,
@@ -486,6 +488,7 @@ impl ThreadRequestProcessor {
486488
pending_thread_unloads,
487489
thread_state_manager,
488490
thread_watch_manager,
491+
auth_transition_lock,
489492
thread_list_state_permit,
490493
thread_goal_processor,
491494
state_db,
@@ -1062,6 +1065,19 @@ impl ThreadRequestProcessor {
10621065
client_mcp_extensions: ClientMcpExtensions,
10631066
request_context: RequestContext,
10641067
) -> Result<(), JSONRPCErrorError> {
1068+
let _auth_transition_guard = self.auth_transition_lock.lock().await;
1069+
reload_auth_from_storage_if_idle(
1070+
&self.auth_manager,
1071+
&self.thread_manager,
1072+
&self.config_manager,
1073+
&self.outgoing,
1074+
&self.thread_watch_manager,
1075+
&self.config.chatgpt_base_url,
1076+
self.config.http_client_factory(),
1077+
"thread/start",
1078+
)
1079+
.await;
1080+
10651081
let ThreadStartParams {
10661082
model,
10671083
model_provider,
@@ -3484,6 +3500,19 @@ impl ThreadRequestProcessor {
34843500
app_server_client_version: Option<String>,
34853501
client_mcp_extensions: ClientMcpExtensions,
34863502
) -> Result<(), JSONRPCErrorError> {
3503+
let _auth_transition_guard = self.auth_transition_lock.lock().await;
3504+
reload_auth_from_storage_if_idle(
3505+
&self.auth_manager,
3506+
&self.thread_manager,
3507+
&self.config_manager,
3508+
&self.outgoing,
3509+
&self.thread_watch_manager,
3510+
&self.config.chatgpt_base_url,
3511+
self.config.http_client_factory(),
3512+
"thread/resume",
3513+
)
3514+
.await;
3515+
34873516
if let Ok(thread_id) = ThreadId::from_string(&params.thread_id)
34883517
&& self
34893518
.pending_thread_unloads

codex-rs/app-server/src/request_processors/turn_processor.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ pub(crate) struct TurnRequestProcessor {
9797
pending_thread_unloads: Arc<Mutex<HashSet<ThreadId>>>,
9898
thread_state_manager: ThreadStateManager,
9999
thread_watch_manager: ThreadWatchManager,
100+
auth_transition_lock: Arc<Mutex<()>>,
100101
thread_list_state_permit: Arc<Semaphore>,
101102
skills_watcher: Arc<SkillsWatcher>,
102103
turn_cost_worker: Option<crate::turn_cost_worker::TurnCostWorkerHandle>,
@@ -153,6 +154,7 @@ impl TurnRequestProcessor {
153154
pending_thread_unloads: Arc<Mutex<HashSet<ThreadId>>>,
154155
thread_state_manager: ThreadStateManager,
155156
thread_watch_manager: ThreadWatchManager,
157+
auth_transition_lock: Arc<Mutex<()>>,
156158
thread_list_state_permit: Arc<Semaphore>,
157159
skills_watcher: Arc<SkillsWatcher>,
158160
turn_cost_worker: Option<crate::turn_cost_worker::TurnCostWorkerHandle>,
@@ -170,6 +172,7 @@ impl TurnRequestProcessor {
170172
pending_thread_unloads,
171173
thread_state_manager,
172174
thread_watch_manager,
175+
auth_transition_lock,
173176
thread_list_state_permit,
174177
skills_watcher,
175178
turn_cost_worker,
@@ -482,6 +485,7 @@ impl TurnRequestProcessor {
482485
app_server_client_name: Option<String>,
483486
app_server_client_version: Option<String>,
484487
) -> Result<TurnStartResponse, JSONRPCErrorError> {
488+
let _auth_transition_guard = self.auth_transition_lock.lock().await;
485489
let (thread_id, thread) =
486490
self.load_thread(&params.thread_id)
487491
.await
@@ -490,6 +494,17 @@ impl TurnRequestProcessor {
490494
})?;
491495
self.ensure_direct_input_allowed(&request_id, thread.as_ref())
492496
.await?;
497+
reload_auth_from_storage_if_idle(
498+
&self.auth_manager,
499+
&self.thread_manager,
500+
&self.config_manager,
501+
&self.outgoing,
502+
&self.thread_watch_manager,
503+
&self.config.chatgpt_base_url,
504+
self.config.http_client_factory(),
505+
"turn/start",
506+
)
507+
.await;
493508
if let Err(error) = Self::validate_v2_input_limit(&params.input) {
494509
self.track_error_response(
495510
&request_id,
@@ -581,6 +596,10 @@ impl TurnRequestProcessor {
581596
}
582597
};
583598

599+
self.thread_watch_manager
600+
.note_turn_started(&thread_id.to_string())
601+
.await;
602+
584603
if turn_has_input && started {
585604
let config_snapshot = thread.config_snapshot().await;
586605
if config_snapshot.is_primary_environment_configured() {

codex-rs/config/src/types.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,19 @@ pub struct Tui {
778778
#[serde(default)]
779779
pub keymap: TuiKeymap,
780780

781+
/// Optional synthetic user-turn prompt injected after a turn fails with
782+
/// `UsageLimitExceeded`.
783+
///
784+
/// When unset, Codext uses the built-in default recovery prompt.
785+
/// When set to an empty string, Codext disables this automatic recovery turn.
786+
#[serde(default)]
787+
pub usage_limit_resume_prompt: Option<String>,
788+
789+
/// Automatically submit `Continue` after a turn fails with `ServerOverloaded`.
790+
/// Defaults to `true`.
791+
#[serde(default = "default_true")]
792+
pub server_overloaded_resume: bool,
793+
781794
/// Startup tooltip availability NUX state persisted by the TUI.
782795
#[serde(default)]
783796
pub model_availability_nux: ModelAvailabilityNuxConfig,

0 commit comments

Comments
 (0)