Files
codex/codex-rs/tui/src/session_queue_commands.rs
T
Eric Traut 4fc522f5e0 Add optional thread resumption to codex queue (#57232)
## What changed

Add `codex queue --resume` to resume an unloaded thread after queueing a message through the local daemon or selected `--remote` server. Already loaded threads are left running. If resumption fails, emit a warning while preserving the successful queue result.

Read only the thread status when checking whether to resume, allowing remote metadata to contain paths from another operating system.

## Testing

Extend CLI integration coverage for unloaded threads consuming queued messages, archived thread rejection, and successful queueing when another server owns the thread and rejects resumption.

GitOrigin-RevId: cc8aa3c11a37e990da36e6ff9a17934f73322e7b
2026-10-26 20:25:22 +00:00

204 lines
7.2 KiB
Rust

//! Queue user messages through a local or remote app server.
use crate::app_server_session::AppServerSession;
use crate::named_session_lookup::SessionCollection;
use crate::named_session_lookup::lookup;
use crate::session_archive_commands::SessionArchiveCommandOptions;
use crate::session_archive_commands::start_app_server_for_session_command;
use codex_app_server_client::TypedRequestError;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ThreadQueueAddParams;
use codex_app_server_protocol::ThreadQueueAddResponse;
use codex_app_server_protocol::ThreadReadParams;
use codex_app_server_protocol::ThreadResumeParams;
use codex_app_server_protocol::ThreadStatus;
use codex_app_server_protocol::UserInput;
use codex_app_server_protocol::experimental_required_message;
use codex_protocol::ThreadId;
use codex_utils_home_dir::find_codex_home;
use color_eyre::Report;
use color_eyre::eyre::Result;
use color_eyre::eyre::WrapErr;
use color_eyre::eyre::eyre;
use serde::Deserialize;
use serde::de::IgnoredAny;
use std::io::Write;
use std::path::Path;
use uuid::Uuid;
const JSONRPC_INVALID_REQUEST: i64 = -32600;
const JSONRPC_METHOD_NOT_FOUND: i64 = -32601;
const THREAD_QUEUE_ADD_METHOD: &str = "thread/queue/add";
pub async fn run_session_queue_command(
target: String,
message: String,
resume: bool,
options: SessionArchiveCommandOptions,
) -> Result<String> {
if options.cli.no_daemon && options.explicit_remote_endpoint.is_none() {
return Err(eyre!(
"--no-daemon cannot be used with codex queue. Queuing must discover the shared server to avoid writing through a separate server."
));
}
let codex_home = find_codex_home().wrap_err("failed to find Codex home")?;
let explicit_remote = options.explicit_remote_endpoint.is_some();
let mut app_server =
start_app_server_for_session_command(options, codex_home.to_path_buf()).await?;
if !explicit_remote
&& app_server.uses_embedded_app_server()
&& super::maybe_probe_default_daemon_socket(codex_home.as_path())
.await
.is_some()
{
return Err(eyre!(
"cannot queue through an embedded app server while a local app-server daemon is running; remove configuration overrides or use --remote"
));
}
let implicit_local_daemon = !explicit_remote && !app_server.uses_embedded_app_server();
let client_message_id = Uuid::now_v7().to_string();
let (thread_id, response) = match run_session_queue_action_with_app_server(
&mut app_server,
codex_home.as_path(),
&target,
&message,
&client_message_id,
)
.await
{
Err(error)
if (implicit_local_daemon || explicit_remote) && is_unsupported_queue_error(&error) =>
{
let server = if explicit_remote {
"remote app server"
} else {
"local app-server daemon"
};
return Err(error.wrap_err(format!(
"the {server} does not support thread/queue/add; update or restart the {server}"
)));
}
result => result?,
};
if resume && let Err(err) = resume_if_unloaded(&mut app_server, thread_id).await {
let _ = writeln!(
std::io::stderr().lock(),
"Warning: message was queued, but could not resume thread {thread_id}: {err:#}"
);
}
Ok(format!(
"Queued message {} for thread {}.",
response.queued_submission.id, thread_id
))
}
pub(super) async fn run_session_queue_action_with_app_server(
app_server: &mut AppServerSession,
codex_home: &Path,
target: &str,
message: &str,
client_message_id: &str,
) -> Result<(ThreadId, ThreadQueueAddResponse)> {
let thread_id = if let Ok(thread_id) = ThreadId::from_string(target) {
thread_id
} else {
let thread = lookup(
app_server,
codex_home,
target,
&[SessionCollection::Active],
&[
super::resume_source_kinds(/*include_non_interactive*/ true),
// An empty filter includes Atlas/ChatGPT sessions.
Vec::new(),
],
/*model_provider*/ None,
)
.await?
.ok_or_else(|| eyre!("No active session found matching '{target}'."))?;
ThreadId::from_string(&thread.id)
.wrap_err_with(|| format!("app server returned invalid session id `{}`", thread.id))?
};
let request_id = app_server.next_request_id();
let response = app_server
.request_handle()
.request_typed(ClientRequest::ThreadQueueAdd {
request_id,
params: ThreadQueueAddParams {
thread_id: thread_id.to_string(),
input: vec![UserInput::Text {
text: message.to_string(),
text_elements: Vec::new(),
}],
client_user_message_id: client_message_id.to_string(),
},
})
.await
.wrap_err("failed to queue session message")?;
Ok((thread_id, response))
}
async fn resume_if_unloaded(app_server: &mut AppServerSession, thread_id: ThreadId) -> Result<()> {
if app_server.uses_embedded_app_server() {
return Err(eyre!(
"start a local daemon or specify a server with --remote"
));
}
// Only decode the status: remote thread metadata can contain paths from another OS.
#[derive(Deserialize)]
struct ReadResponse {
thread: Status,
}
#[derive(Deserialize)]
struct Status {
status: ThreadStatus,
}
let handle = app_server.request_handle();
let response: ReadResponse = handle
.request_typed(ClientRequest::ThreadRead {
request_id: app_server.next_request_id(),
params: ThreadReadParams {
thread_id: thread_id.to_string(),
include_turns: false,
},
})
.await
.wrap_err_with(|| format!("failed to read thread {thread_id}"))?;
if response.thread.status != ThreadStatus::NotLoaded {
// A fresh loaded thread can accept input before it has any history to resume.
return Ok(());
}
handle
.request_typed::<IgnoredAny>(ClientRequest::ThreadResume {
request_id: app_server.next_request_id(),
params: ThreadResumeParams {
thread_id: thread_id.to_string(),
exclude_turns: true,
..Default::default()
},
})
.await?;
Ok(())
}
fn is_unsupported_queue_error(error: &Report) -> bool {
matches!(
error.downcast_ref::<TypedRequestError>(),
Some(TypedRequestError::Server { method, source })
if method == THREAD_QUEUE_ADD_METHOD
&& (source.code == JSONRPC_METHOD_NOT_FOUND
|| (source.code == JSONRPC_INVALID_REQUEST
&& (source.message == experimental_required_message(THREAD_QUEUE_ADD_METHOD)
|| source.message.starts_with(
"Invalid request: unknown variant `thread/queue/add`",
))))
)
}
#[cfg(test)]
#[path = "session_queue_commands_tests.rs"]
mod tests;