Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
506 changes: 506 additions & 0 deletions crates/cli/src/agents/codex/history.rs

Large diffs are not rendered by default.

68 changes: 66 additions & 2 deletions crates/cli/src/agents/codex/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,74 @@ use crate::agents::CodingAgent;
use crate::error::CliError;
use crate::installation::{InstallRequest, UninstallRequest};

use super::history;

pub(crate) fn install(command: InstallRequest) -> Result<ExitCode, CliError> {
crate::installation::marketplace::install(CodingAgent::Codex, command)
let migrate_history = command.migrate_history;
let dry_run = command.dry_run;
let history_database = command.history_database.clone();
let status = crate::installation::marketplace::install(CodingAgent::Codex, command)?;
if status != ExitCode::SUCCESS || !migrate_history {
return Ok(status);
}
// The provider must exist in config.toml before threads are pointed at it,
// so migrate only after the install itself has succeeded.
//
// A failure here is reported as a nonzero status rather than an error: the
// integration is installed and working, and calling it an install failure
// would send the caller to the wrong remedy.
match history::migrate_to_relay(dry_run, history_database.as_deref()) {
Ok(_) => Ok(status),
Err(error) => {
log::error!(
target: "nemo_relay.installation",
event = "codex_history_migration_failed",
host = "codex",
error_kind = "history_migration";
"Codex integration installed but thread-history migration failed"
);
println!("the Codex integration is installed, but history migration failed: {error}");
println!(
"retry the migration with `nemo-relay install codex --force --migrate-history`."
);
Ok(ExitCode::FAILURE)
}
}
}

pub(crate) fn uninstall(command: UninstallRequest) -> Result<ExitCode, CliError> {
crate::installation::marketplace::uninstall(CodingAgent::Codex, command)
let skip_history_migration = command.skip_history_migration;
let dry_run = command.dry_run;
let history_database = command.history_database.clone();
let status = crate::installation::marketplace::uninstall(CodingAgent::Codex, command)?;
if status != ExitCode::SUCCESS || skip_history_migration {
return Ok(status);
}
// Reversal is inferred from the migration journal rather than a flag, so a
// user who migrated at install time does not have to remember to ask for it
// again here.
//
// As with install, a failure here is a nonzero status rather than an error.
// The integration is already removed; the journal survives so a later
// uninstall can still reverse the migration.
match history::restore_from_relay(dry_run, history_database.as_deref()) {
Ok(_) => Ok(status),
Err(error) => {
log::error!(
target: "nemo_relay.installation",
event = "codex_history_restore_failed",
host = "codex",
error_kind = "history_migration";
"Codex integration uninstalled but thread-history restore failed"
);
println!(
"the Codex integration is uninstalled, but restoring thread history failed: {error}"
);
println!(
"thread history is still recorded under the Relay provider; resuming those threads \
fails until it is restored."
);
Ok(ExitCode::FAILURE)
}
}
}
1 change: 1 addition & 0 deletions crates/cli/src/agents/codex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use super::AgentDescriptor;
pub(super) mod app_server;
pub(super) mod assets;
pub(crate) mod doctor;
pub(super) mod history;
pub(super) mod host;
pub(crate) mod install;
pub(crate) mod launch;
Expand Down
47 changes: 47 additions & 0 deletions crates/cli/src/commands/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ pub(crate) struct InstallCommand {
pub(crate) dry_run: bool,
#[arg(long)]
pub(crate) skip_doctor: bool,
/// Experimental: move existing Codex thread history onto the Relay provider so it stays
/// visible in the Codex resume picker. `nemo-relay uninstall codex` reverses this
/// automatically. Requires the `sqlite3` command on PATH, which Windows does not provide by
/// default.
#[arg(long)]
pub(crate) migrate_history: bool,
/// Codex thread database to migrate, when Codex has moved past the default schema
/// generation. Accepts a bare file name such as `state_6.sqlite`, resolved inside the Codex
/// home, or a full path. Requires `--migrate-history`.
#[arg(long, value_name = "PATH")]
pub(crate) history_database: Option<PathBuf>,
}

#[derive(Debug, Clone, Args)]
Expand All @@ -34,6 +45,13 @@ pub(crate) struct UninstallCommand {
pub(crate) force: bool,
#[arg(long)]
pub(crate) dry_run: bool,
/// Leave migrated Codex thread history on the Relay provider instead of restoring it.
#[arg(long)]
pub(crate) skip_history_migration: bool,
/// Codex thread database to restore, overriding the one the migration recorded. Accepts a bare
/// file name such as `state_6.sqlite`, resolved inside the Codex home, or a full path.
#[arg(long, value_name = "PATH")]
pub(crate) history_database: Option<PathBuf>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
Expand Down Expand Up @@ -66,6 +84,8 @@ impl InstallCommand {
force: self.force,
dry_run: self.dry_run,
skip_doctor: self.skip_doctor,
migrate_history: self.migrate_history,
history_database: self.history_database,
}
}
}
Expand All @@ -76,12 +96,24 @@ impl UninstallCommand {
install_dir: self.install_dir,
force: self.force,
dry_run: self.dry_run,
skip_history_migration: self.skip_history_migration,
history_database: self.history_database,
}
}
}

pub(super) fn install(command: InstallCommand) -> Result<ExitCode, CliError> {
let target = command.host;
if command.migrate_history && matches!(target, InstallTarget::ClaudeCode) {
return Err(CliError::Install(
"--migrate-history applies to the Codex integration only".into(),
));
}
if command.history_database.is_some() && !command.migrate_history {
return Err(CliError::Install(
"--history-database requires --migrate-history".into(),
));
}
let request = command.into_runtime();
let candidates = target.agents();
let agents = if target.is_all() {
Expand All @@ -104,6 +136,21 @@ pub(super) fn install(command: InstallCommand) -> Result<ExitCode, CliError> {

pub(super) fn uninstall(command: UninstallCommand) -> Result<ExitCode, CliError> {
let target = command.host;
if command.skip_history_migration && matches!(target, InstallTarget::ClaudeCode) {
return Err(CliError::Install(
"--skip-history-migration applies to the Codex integration only".into(),
));
}
if command.history_database.is_some() && matches!(target, InstallTarget::ClaudeCode) {
return Err(CliError::Install(
"--history-database applies to the Codex integration only".into(),
));
}
if command.history_database.is_some() && command.skip_history_migration {
return Err(CliError::Install(
"--history-database cannot be combined with --skip-history-migration".into(),
));
}
let request = command.into_runtime();
let candidates = target.agents();
let agents = if target.is_all() {
Expand Down
4 changes: 4 additions & 0 deletions crates/cli/src/commands/integrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ fn refresh(command: RefreshCommand) -> Result<ExitCode, CliError> {
force: true,
dry_run: command.dry_run,
skip_doctor: false,
// Refresh repairs an existing installation; any recorded history
// migration stays recorded and does not need to run again.
migrate_history: false,
history_database: None,
};
let result = match crate::agents::install_integration(agent, request) {
Ok(status) if status == ExitCode::SUCCESS => Ok(()),
Expand Down
8 changes: 8 additions & 0 deletions crates/cli/src/installation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@ pub(crate) struct InstallRequest {
pub(crate) force: bool,
pub(crate) dry_run: bool,
pub(crate) skip_doctor: bool,
/// Experimental: rewrite pre-install Codex thread history onto the Relay provider.
pub(crate) migrate_history: bool,
/// Codex thread database to migrate, when it is not the default generation.
pub(crate) history_database: Option<PathBuf>,
}

#[derive(Debug, Clone)]
pub(crate) struct UninstallRequest {
pub(crate) install_dir: Option<PathBuf>,
pub(crate) force: bool,
pub(crate) dry_run: bool,
/// Skip the reversal that a recorded history migration would otherwise infer.
pub(crate) skip_history_migration: bool,
/// Codex thread database to restore, overriding the one the journal recorded.
pub(crate) history_database: Option<PathBuf>,
}
Loading
Loading