mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add app-server background terminal process APIs (#26041)
## Summary Codex Apps needs app-server as the source of truth for chat-started background terminals instead of guessing from local process trees. This PR adds experimental v2 APIs to list and terminate background terminals for a loaded thread using app-server process ids, so clients can manage background terminals without local PID discovery. ## Changes - `thread/backgroundTerminals/list` returns paginated background terminal records with `itemId`, app-server `processId`, `command`, `cwd`, nullable `osPid`, nullable `cpuPercent`, and nullable `rssKb`. - `thread/backgroundTerminals/terminate` terminates one running background terminal by app-server `processId` and returns whether a process was terminated. - Background terminal list and terminate operations use unified-exec process manager state as their source of truth.
This commit is contained in:
@@ -1126,6 +1126,16 @@ impl MessageProcessor {
|
||||
.thread_background_terminals_clean(&request_id, params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadBackgroundTerminalsList { params, .. } => {
|
||||
self.thread_processor
|
||||
.thread_background_terminals_list(params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadBackgroundTerminalsTerminate { params, .. } => {
|
||||
self.thread_processor
|
||||
.thread_background_terminals_terminate(params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadRollback { params, .. } => {
|
||||
self.thread_processor
|
||||
.thread_rollback(&request_id, params)
|
||||
|
||||
@@ -171,8 +171,13 @@ use codex_app_server_protocol::ThreadApproveGuardianDeniedActionResponse;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
use codex_app_server_protocol::ThreadArchiveResponse;
|
||||
use codex_app_server_protocol::ThreadArchivedNotification;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminal;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminalsCleanParams;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminalsCleanResponse;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminalsListParams;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminalsListResponse;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminalsTerminateParams;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminalsTerminateResponse;
|
||||
use codex_app_server_protocol::ThreadClosedNotification;
|
||||
use codex_app_server_protocol::ThreadCompactStartParams;
|
||||
use codex_app_server_protocol::ThreadCompactStartResponse;
|
||||
|
||||
@@ -569,6 +569,24 @@ impl ThreadRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_background_terminals_list(
|
||||
&self,
|
||||
params: ThreadBackgroundTerminalsListParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_background_terminals_list_inner(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_background_terminals_terminate(
|
||||
&self,
|
||||
params: ThreadBackgroundTerminalsTerminateParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_background_terminals_terminate_inner(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_rollback(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
@@ -1725,6 +1743,54 @@ impl ThreadRequestProcessor {
|
||||
Ok(ThreadBackgroundTerminalsCleanResponse {})
|
||||
}
|
||||
|
||||
async fn thread_background_terminals_list_inner(
|
||||
&self,
|
||||
params: ThreadBackgroundTerminalsListParams,
|
||||
) -> Result<ThreadBackgroundTerminalsListResponse, JSONRPCErrorError> {
|
||||
let ThreadBackgroundTerminalsListParams {
|
||||
thread_id,
|
||||
cursor,
|
||||
limit,
|
||||
} = params;
|
||||
|
||||
let (_, thread) = self.load_thread(&thread_id).await?;
|
||||
let terminals = thread
|
||||
.list_background_terminals()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|terminal| ThreadBackgroundTerminal {
|
||||
item_id: terminal.item_id,
|
||||
process_id: terminal.process_id,
|
||||
command: terminal.command,
|
||||
cwd: terminal.cwd,
|
||||
os_pid: None,
|
||||
cpu_percent: None,
|
||||
rss_kb: None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (data, next_cursor) = paginate_background_terminals(&terminals, cursor, limit)?;
|
||||
|
||||
Ok(ThreadBackgroundTerminalsListResponse { data, next_cursor })
|
||||
}
|
||||
|
||||
async fn thread_background_terminals_terminate_inner(
|
||||
&self,
|
||||
params: ThreadBackgroundTerminalsTerminateParams,
|
||||
) -> Result<ThreadBackgroundTerminalsTerminateResponse, JSONRPCErrorError> {
|
||||
let ThreadBackgroundTerminalsTerminateParams {
|
||||
thread_id,
|
||||
process_id,
|
||||
} = params;
|
||||
let process_id = process_id.parse::<i32>().map_err(|err| {
|
||||
invalid_request(format!("invalid background terminal process id: {err}"))
|
||||
})?;
|
||||
|
||||
let (_, thread) = self.load_thread(&thread_id).await?;
|
||||
let terminated = thread.terminate_background_terminal(process_id).await;
|
||||
Ok(ThreadBackgroundTerminalsTerminateResponse { terminated })
|
||||
}
|
||||
|
||||
async fn thread_shell_command_inner(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
@@ -4250,6 +4316,34 @@ fn build_thread_from_snapshot(
|
||||
}
|
||||
}
|
||||
|
||||
fn paginate_background_terminals(
|
||||
terminals: &[ThreadBackgroundTerminal],
|
||||
cursor: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<(Vec<ThreadBackgroundTerminal>, Option<String>), JSONRPCErrorError> {
|
||||
let start = match cursor {
|
||||
Some(cursor) => {
|
||||
let cursor = cursor
|
||||
.parse::<i32>()
|
||||
.map_err(|err| invalid_request(format!("invalid cursor: {err}")))?;
|
||||
terminals
|
||||
.iter()
|
||||
.position(|terminal| {
|
||||
terminal
|
||||
.process_id
|
||||
.parse::<i32>()
|
||||
.is_ok_and(|process_id| process_id > cursor)
|
||||
})
|
||||
.unwrap_or(terminals.len())
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
let effective_limit = limit.unwrap_or(terminals.len() as u32).max(1) as usize;
|
||||
let end = start.saturating_add(effective_limit).min(terminals.len());
|
||||
let next_cursor = (end < terminals.len()).then(|| terminals[end - 1].process_id.clone());
|
||||
Ok((terminals[start..end].to_vec(), next_cursor))
|
||||
}
|
||||
|
||||
fn build_thread_from_loaded_snapshot(
|
||||
thread_id: ThreadId,
|
||||
config_snapshot: &ThreadConfigSnapshot,
|
||||
|
||||
@@ -36,6 +36,65 @@ mod thread_list_cwd_filter_tests {
|
||||
}
|
||||
}
|
||||
|
||||
mod background_terminal_pagination_tests {
|
||||
use super::super::paginate_background_terminals;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminal;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn terminal(process_id: &str) -> ThreadBackgroundTerminal {
|
||||
let cwd = if cfg!(windows) { r"C:\tmp" } else { "/tmp" };
|
||||
|
||||
ThreadBackgroundTerminal {
|
||||
item_id: format!("item-{process_id}"),
|
||||
process_id: process_id.to_string(),
|
||||
command: format!("command-{process_id}"),
|
||||
cwd: AbsolutePathBuf::from_absolute_path(cwd).expect("absolute cwd"),
|
||||
os_pid: None,
|
||||
cpu_percent: None,
|
||||
rss_kb: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paginates_with_process_id_cursor() {
|
||||
let terminals = vec![
|
||||
terminal("1"),
|
||||
terminal("2"),
|
||||
terminal("3"),
|
||||
terminal("4"),
|
||||
terminal("5"),
|
||||
];
|
||||
|
||||
let (data, next_cursor) =
|
||||
paginate_background_terminals(&terminals, /*cursor*/ None, Some(2))
|
||||
.expect("valid page");
|
||||
|
||||
assert_eq!(data, vec![terminal("1"), terminal("2")]);
|
||||
assert_eq!(next_cursor, Some("2".to_string()));
|
||||
let first_cursor = next_cursor;
|
||||
|
||||
let terminals_without_anchor = vec![terminal("1"), terminal("3"), terminal("4")];
|
||||
let (data, next_cursor) =
|
||||
paginate_background_terminals(&terminals_without_anchor, first_cursor.clone(), Some(2))
|
||||
.expect("valid page");
|
||||
|
||||
assert_eq!(data, vec![terminal("3"), terminal("4")]);
|
||||
assert_eq!(next_cursor, None);
|
||||
|
||||
let (data, next_cursor) =
|
||||
paginate_background_terminals(&terminals, first_cursor, Some(2)).expect("valid page");
|
||||
|
||||
assert_eq!(data, vec![terminal("3"), terminal("4")]);
|
||||
assert_eq!(next_cursor, Some("4".to_string()));
|
||||
|
||||
assert!(
|
||||
paginate_background_terminals(&terminals, Some("missing".to_string()), Some(1))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod thread_processor_behavior_tests {
|
||||
async fn forked_from_id_from_rollout(path: &Path) -> Option<String> {
|
||||
codex_core::read_session_meta_line(path)
|
||||
|
||||
Reference in New Issue
Block a user