mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
## 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
204 lines
7.2 KiB
Rust
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;
|