mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
## Why Loaded threads, including idle threads whose rollout files are still deferred, need to survive a managed daemon restart. Shutdown must also remain forceable when rollout I/O is blocked. ## What changed - Add a hidden `--managed-daemon` option for app-server Unix socket transports. After active turns and admitted requests drain, save loaded persistent root threads before exiting. - Skip ephemeral threads, non-root agent threads, and threads pending unload. Log persistence failures and continue saving other threads. - Apply shutdown admission checks to thread and turn settings updates, thread deletion, and archiving. - Keep force signals and daemon shutdown requests responsive during persistence. Return `AppServerExit::Forced` so executables can exit without waiting for runtime teardown. ## Testing Add integration coverage for resuming active and idle threads after restart, forcing shutdown during active work, and forcing shutdown with a blocked rollout writer. Extend shutdown rejection coverage to deletion and settings updates. GitOrigin-RevId: 4344e97d39f9f80c5d84c41c17300fd6aa99b4cc
168 lines
5.4 KiB
Rust
168 lines
5.4 KiB
Rust
#![recursion_limit = "256"]
|
|
|
|
use clap::Parser;
|
|
use codex_app_server::AppServerCodeModeHostArgs;
|
|
use codex_app_server::AppServerRuntimeOptions;
|
|
use codex_app_server::AppServerTransport;
|
|
use codex_app_server::AppServerWebsocketAuthArgs;
|
|
use codex_app_server::PluginStartupTasks;
|
|
use codex_app_server::run_main_with_transport_options;
|
|
use codex_arg0::Arg0DispatchPaths;
|
|
use codex_arg0::arg0_dispatch_or_else;
|
|
use codex_config::LoaderOverrides;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_utils_cli::CliConfigOverrides;
|
|
use std::path::PathBuf;
|
|
|
|
#[cfg(all(
|
|
target_os = "linux",
|
|
target_env = "musl",
|
|
any(target_arch = "x86_64", target_arch = "aarch64")
|
|
))]
|
|
#[global_allocator]
|
|
static ALLOCATOR: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
|
|
|
// Debug-only test hook: lets integration tests point the server at a temporary
|
|
// managed config file without writing to /etc.
|
|
const MANAGED_CONFIG_PATH_ENV_VAR: &str = "CODEX_APP_SERVER_MANAGED_CONFIG_PATH";
|
|
const DISABLE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG";
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[command(version)]
|
|
struct AppServerArgs {
|
|
#[command(flatten)]
|
|
config_overrides: CliConfigOverrides,
|
|
|
|
#[command(flatten)]
|
|
code_mode_host: AppServerCodeModeHostArgs,
|
|
|
|
/// Transport endpoint URL. Supported values: `stdio://` (default),
|
|
/// `unix://`, `unix://PATH`, `ws://IP:PORT`, `off`.
|
|
#[arg(
|
|
long = "listen",
|
|
value_name = "URL",
|
|
default_value = AppServerTransport::DEFAULT_LISTEN_URL
|
|
)]
|
|
listen: AppServerTransport,
|
|
|
|
/// Session source used to derive product restrictions and metadata.
|
|
#[arg(
|
|
long = "session-source",
|
|
value_name = "SOURCE",
|
|
default_value = "vscode",
|
|
value_parser = SessionSource::from_startup_arg
|
|
)]
|
|
session_source: SessionSource,
|
|
|
|
#[command(flatten)]
|
|
auth: AppServerWebsocketAuthArgs,
|
|
|
|
/// Fail if config.toml contains unknown configuration fields.
|
|
#[arg(long = "strict-config", default_value_t = false)]
|
|
strict_config: bool,
|
|
|
|
/// Hidden debug-only test hook used by integration tests that spawn the
|
|
/// production app-server binary.
|
|
#[cfg(debug_assertions)]
|
|
#[arg(long = "disable-plugin-startup-tasks-for-tests", hide = true)]
|
|
disable_plugin_startup_tasks_for_tests: bool,
|
|
|
|
/// Enable remote control for this app-server process without changing persistence.
|
|
#[arg(long = "remote-control", hide = true)]
|
|
remote_control: bool,
|
|
|
|
/// Save loaded threads during managed daemon shutdown.
|
|
#[arg(long, hide = true)]
|
|
managed_daemon: bool,
|
|
}
|
|
|
|
fn main() -> anyhow::Result<()> {
|
|
let remote_control_disabled = codex_app_server::take_remote_control_disabled_env();
|
|
arg0_dispatch_or_else(move |arg0_paths: Arg0DispatchPaths| async move {
|
|
let AppServerArgs {
|
|
config_overrides,
|
|
code_mode_host,
|
|
listen,
|
|
session_source,
|
|
auth,
|
|
strict_config,
|
|
#[cfg(debug_assertions)]
|
|
disable_plugin_startup_tasks_for_tests,
|
|
remote_control,
|
|
managed_daemon,
|
|
} = AppServerArgs::parse();
|
|
let loader_overrides = if disable_managed_config_from_debug_env() {
|
|
LoaderOverrides::without_managed_config_for_tests()
|
|
} else {
|
|
managed_config_path_from_debug_env()
|
|
.map(LoaderOverrides::with_managed_config_path_for_tests)
|
|
.unwrap_or_default()
|
|
};
|
|
let transport = listen;
|
|
let auth = auth.try_into_settings()?;
|
|
let mut runtime_options = AppServerRuntimeOptions {
|
|
code_mode_host_transport: code_mode_host.into(),
|
|
managed_daemon,
|
|
..Default::default()
|
|
};
|
|
#[cfg(debug_assertions)]
|
|
if disable_plugin_startup_tasks_for_tests {
|
|
runtime_options.plugin_startup_tasks = PluginStartupTasks::Skip;
|
|
}
|
|
runtime_options.remote_control_startup_mode =
|
|
match (remote_control, remote_control_disabled) {
|
|
(true, _) => codex_app_server::RemoteControlStartupMode::EnabledEphemeral,
|
|
(false, true) => codex_app_server::RemoteControlStartupMode::DisabledEphemeral,
|
|
(false, false) => codex_app_server::RemoteControlStartupMode::ResolvePersisted,
|
|
};
|
|
|
|
let exit = run_main_with_transport_options(
|
|
arg0_paths,
|
|
config_overrides,
|
|
loader_overrides,
|
|
strict_config,
|
|
/*default_analytics_enabled*/ false,
|
|
transport,
|
|
session_source,
|
|
auth,
|
|
runtime_options,
|
|
)
|
|
.await?;
|
|
if exit == codex_app_server::AppServerExit::Forced {
|
|
// Runtime teardown can wait forever for blocked rollout I/O.
|
|
std::process::exit(0);
|
|
}
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn disable_managed_config_from_debug_env() -> bool {
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
if let Ok(value) = std::env::var(DISABLE_MANAGED_CONFIG_ENV_VAR) {
|
|
return matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES");
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
fn managed_config_path_from_debug_env() -> Option<PathBuf> {
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
if let Ok(value) = std::env::var(MANAGED_CONFIG_PATH_ENV_VAR) {
|
|
return if value.is_empty() {
|
|
None
|
|
} else {
|
|
Some(PathBuf::from(value))
|
|
};
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "main_tests.rs"]
|
|
mod tests;
|