diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 158443ee49..c72d730a8b 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3538,6 +3538,7 @@ mod tests { aggregated_output: StreamOutput::new("Command output".to_string()), duration: StdDuration::from_secs(1), timed_out: true, + os_pid: None, }; let (_, turn_context) = make_session_and_context().await; diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 3b490436e4..163bcb5c43 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -636,6 +636,7 @@ mod tests { aggregated_output: StreamOutput::new("aggregate detail".to_string()), duration: Duration::from_millis(10), timed_out: false, + os_pid: None, }; let err = CodexErr::Sandbox(SandboxErr::Denied { output: Box::new(output), @@ -652,6 +653,7 @@ mod tests { aggregated_output: StreamOutput::new(String::new()), duration: Duration::from_millis(10), timed_out: false, + os_pid: None, }; let err = CodexErr::Sandbox(SandboxErr::Denied { output: Box::new(output), @@ -668,6 +670,7 @@ mod tests { aggregated_output: StreamOutput::new(String::new()), duration: Duration::from_millis(8), timed_out: false, + os_pid: None, }; let err = CodexErr::Sandbox(SandboxErr::Denied { output: Box::new(output), @@ -711,6 +714,7 @@ mod tests { aggregated_output: StreamOutput::new(String::new()), duration: Duration::from_millis(5), timed_out: false, + os_pid: None, }; let err = CodexErr::Sandbox(SandboxErr::Denied { output: Box::new(output), diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 2a91873238..9263d31825 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -313,6 +313,7 @@ async fn exec_windows_sandbox( stderr, aggregated_output, timed_out: capture.timed_out, + os_pid: None, }) } @@ -352,6 +353,7 @@ fn finalize_exec_result( aggregated_output, duration, timed_out, + os_pid: raw_output.os_pid, }; if timed_out { @@ -469,6 +471,7 @@ struct RawExecToolCallOutput { pub stderr: StreamOutput>, pub aggregated_output: StreamOutput>, pub timed_out: bool, + pub os_pid: Option, } impl StreamOutput { @@ -502,6 +505,7 @@ pub struct ExecToolCallOutput { pub aggregated_output: StreamOutput, pub duration: Duration, pub timed_out: bool, + pub os_pid: Option, } impl Default for ExecToolCallOutput { @@ -513,6 +517,7 @@ impl Default for ExecToolCallOutput { aggregated_output: StreamOutput::new(String::new()), duration: Duration::ZERO, timed_out: false, + os_pid: None, } } } @@ -573,6 +578,7 @@ async fn consume_truncated_output( // above, therefore `take()` should normally return `Some`. If it doesn't // we treat it as an exceptional I/O error + let os_pid = child.id(); let stdout_reader = child.stdout.take().ok_or_else(|| { CodexErr::Io(io::Error::other( "stdout pipe was unexpectedly not available", @@ -680,6 +686,7 @@ async fn consume_truncated_output( stderr, aggregated_output, timed_out, + os_pid, }) } @@ -769,6 +776,7 @@ mod tests { aggregated_output: StreamOutput::new(aggregated.to_string()), duration: Duration::from_millis(1), timed_out: false, + os_pid: None, } } diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 2b7ae53d0c..39f7de8ede 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -89,6 +89,7 @@ impl SessionTask for UserShellCommandTask { EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id: call_id.clone(), process_id: None, + os_pid: None, turn_id: turn_context.sub_id.clone(), command: command.clone(), cwd: cwd.clone(), @@ -133,6 +134,7 @@ impl SessionTask for UserShellCommandTask { aggregated_output: StreamOutput::new(aborted_message.clone()), duration: Duration::ZERO, timed_out: false, + os_pid: None, }; let output_items = [user_shell_command_record_item( &raw_command, @@ -148,6 +150,7 @@ impl SessionTask for UserShellCommandTask { EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, process_id: None, + os_pid: None, turn_id: turn_context.sub_id.clone(), command: command.clone(), cwd: cwd.clone(), @@ -171,6 +174,7 @@ impl SessionTask for UserShellCommandTask { EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: call_id.clone(), process_id: None, + os_pid: output.os_pid, turn_id: turn_context.sub_id.clone(), command: command.clone(), cwd: cwd.clone(), @@ -209,6 +213,7 @@ impl SessionTask for UserShellCommandTask { aggregated_output: StreamOutput::new(message.clone()), duration: Duration::ZERO, timed_out: false, + os_pid: None, }; session .send_event( @@ -216,6 +221,7 @@ impl SessionTask for UserShellCommandTask { EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, process_id: None, + os_pid: None, turn_id: turn_context.sub_id.clone(), command, cwd, diff --git a/codex-rs/core/src/tools/events.rs b/codex-rs/core/src/tools/events.rs index ba720918b7..5f2d1e9bcd 100644 --- a/codex-rs/core/src/tools/events.rs +++ b/codex-rs/core/src/tools/events.rs @@ -58,6 +58,7 @@ pub(crate) enum ToolEventFailure { Message(String), } +#[allow(clippy::too_many_arguments)] pub(crate) async fn emit_exec_command_begin( ctx: ToolEventCtx<'_>, command: &[String], @@ -66,21 +67,22 @@ pub(crate) async fn emit_exec_command_begin( source: ExecCommandSource, interaction_input: Option, process_id: Option<&str>, + os_pid: Option, ) { + let event = ExecCommandBeginEvent { + call_id: ctx.call_id.to_string(), + process_id: process_id.map(str::to_owned), + os_pid, + turn_id: ctx.turn.sub_id.clone(), + command: command.to_vec(), + cwd: cwd.to_path_buf(), + parsed_cmd: parsed_cmd.to_vec(), + source, + interaction_input, + }; + record_exec_span_metadata(process_id, os_pid); ctx.session - .send_event( - ctx.turn, - EventMsg::ExecCommandBegin(ExecCommandBeginEvent { - call_id: ctx.call_id.to_string(), - process_id: process_id.map(str::to_owned), - turn_id: ctx.turn.sub_id.clone(), - command: command.to_vec(), - cwd: cwd.to_path_buf(), - parsed_cmd: parsed_cmd.to_vec(), - source, - interaction_input, - }), - ) + .send_event(ctx.turn, EventMsg::ExecCommandBegin(event)) .await; } // Concrete, allocation-free emitter: avoid trait objects and boxed futures. @@ -102,6 +104,7 @@ pub(crate) enum ToolEmitter { source: ExecCommandSource, parsed_cmd: Vec, process_id: Option, + os_pid: Option, }, } @@ -134,6 +137,7 @@ impl ToolEmitter { cwd: PathBuf, source: ExecCommandSource, process_id: Option, + os_pid: Option, ) -> Self { let parsed_cmd = parse_command(command); Self::UnifiedExec { @@ -142,6 +146,7 @@ impl ToolEmitter { source, parsed_cmd, process_id, + os_pid, } } @@ -159,7 +164,15 @@ impl ToolEmitter { ) => { emit_exec_stage( ctx, - ExecCommandInput::new(command, cwd.as_path(), parsed_cmd, *source, None, None), + ExecCommandInput::new( + command, + cwd.as_path(), + parsed_cmd, + *source, + None, + None, + None, + ), stage, ) .await; @@ -231,6 +244,7 @@ impl ToolEmitter { source, parsed_cmd, process_id, + os_pid, }, stage, ) => { @@ -243,6 +257,7 @@ impl ToolEmitter { *source, None, process_id.as_deref(), + *os_pid, ), stage, ) @@ -328,6 +343,7 @@ struct ExecCommandInput<'a> { source: ExecCommandSource, interaction_input: Option<&'a str>, process_id: Option<&'a str>, + os_pid: Option, } impl<'a> ExecCommandInput<'a> { @@ -338,6 +354,7 @@ impl<'a> ExecCommandInput<'a> { source: ExecCommandSource, interaction_input: Option<&'a str>, process_id: Option<&'a str>, + os_pid: Option, ) -> Self { Self { command, @@ -346,6 +363,7 @@ impl<'a> ExecCommandInput<'a> { source, interaction_input, process_id, + os_pid, } } } @@ -357,6 +375,7 @@ struct ExecCommandResult { exit_code: i32, duration: Duration, formatted_output: String, + os_pid: Option, } async fn emit_exec_stage( @@ -374,11 +393,13 @@ async fn emit_exec_stage( exec_input.source, exec_input.interaction_input.map(str::to_owned), exec_input.process_id, + exec_input.os_pid, ) .await; } ToolEventStage::Success(output) | ToolEventStage::Failure(ToolEventFailure::Output(output)) => { + let os_pid = exec_input.os_pid.or(output.os_pid); let exec_result = ExecCommandResult { stdout: output.stdout.text.clone(), stderr: output.stderr.text.clone(), @@ -386,6 +407,7 @@ async fn emit_exec_stage( exit_code: output.exit_code, duration: output.duration, formatted_output: format_exec_output_str(&output, ctx.turn.truncation_policy), + os_pid, }; emit_exec_end(ctx, exec_input, exec_result).await; } @@ -398,6 +420,7 @@ async fn emit_exec_stage( exit_code: -1, duration: Duration::ZERO, formatted_output: text, + os_pid: exec_input.os_pid, }; emit_exec_end(ctx, exec_input, exec_result).await; } @@ -409,29 +432,45 @@ async fn emit_exec_end( exec_input: ExecCommandInput<'_>, exec_result: ExecCommandResult, ) { + let event = ExecCommandEndEvent { + call_id: ctx.call_id.to_string(), + process_id: exec_input.process_id.map(str::to_owned), + os_pid: exec_result.os_pid, + turn_id: ctx.turn.sub_id.clone(), + command: exec_input.command.to_vec(), + cwd: exec_input.cwd.to_path_buf(), + parsed_cmd: exec_input.parsed_cmd.to_vec(), + source: exec_input.source, + interaction_input: exec_input.interaction_input.map(str::to_owned), + stdout: exec_result.stdout, + stderr: exec_result.stderr, + aggregated_output: exec_result.aggregated_output, + exit_code: exec_result.exit_code, + duration: exec_result.duration, + formatted_output: exec_result.formatted_output, + }; + record_exec_span_metadata( + exec_input.process_id, + exec_result.os_pid.or(exec_input.os_pid), + ); ctx.session - .send_event( - ctx.turn, - EventMsg::ExecCommandEnd(ExecCommandEndEvent { - call_id: ctx.call_id.to_string(), - process_id: exec_input.process_id.map(str::to_owned), - turn_id: ctx.turn.sub_id.clone(), - command: exec_input.command.to_vec(), - cwd: exec_input.cwd.to_path_buf(), - parsed_cmd: exec_input.parsed_cmd.to_vec(), - source: exec_input.source, - interaction_input: exec_input.interaction_input.map(str::to_owned), - stdout: exec_result.stdout, - stderr: exec_result.stderr, - aggregated_output: exec_result.aggregated_output, - exit_code: exec_result.exit_code, - duration: exec_result.duration, - formatted_output: exec_result.formatted_output, - }), - ) + .send_event(ctx.turn, EventMsg::ExecCommandEnd(event)) .await; } +fn record_exec_span_metadata(process_id: Option<&str>, os_pid: Option) { + let span = tracing::Span::current(); + if span.is_disabled() { + return; + } + if let Some(process_id) = process_id { + span.record("process_id", process_id); + } + if let Some(os_pid) = os_pid { + span.record("os_pid", os_pid); + } +} + async fn emit_patch_end( ctx: ToolEventCtx<'_>, changes: HashMap, diff --git a/codex-rs/core/src/tools/parallel.rs b/codex-rs/core/src/tools/parallel.rs index dcd3ae40ad..3880522a80 100644 --- a/codex-rs/core/src/tools/parallel.rs +++ b/codex-rs/core/src/tools/parallel.rs @@ -6,6 +6,7 @@ use tokio_util::either::Either; use tokio_util::sync::CancellationToken; use tokio_util::task::AbortOnDropHandle; use tracing::Instrument; +use tracing::field; use tracing::instrument; use tracing::trace_span; @@ -66,6 +67,8 @@ impl ToolCallRuntime { tool_name = call.tool_name.as_str(), call_id = call.call_id.as_str(), aborted = false, + process_id = field::Empty, + os_pid = field::Empty, ); let handle: AbortOnDropHandle> = diff --git a/codex-rs/core/src/unified_exec/async_watcher.rs b/codex-rs/core/src/unified_exec/async_watcher.rs index 1fbb1e8f6d..4b57520418 100644 --- a/codex-rs/core/src/unified_exec/async_watcher.rs +++ b/codex-rs/core/src/unified_exec/async_watcher.rs @@ -111,6 +111,7 @@ pub(crate) fn spawn_exit_watcher( command: Vec, cwd: PathBuf, process_id: String, + os_pid: Option, transcript: Arc>, started_at: Instant, ) { @@ -130,6 +131,7 @@ pub(crate) fn spawn_exit_watcher( command, cwd, Some(process_id), + os_pid, transcript, String::new(), exit_code, @@ -182,6 +184,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec( command: Vec, cwd: PathBuf, process_id: Option, + os_pid: Option, transcript: Arc>, fallback_output: String, exit_code: i32, @@ -195,6 +198,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec( aggregated_output: StreamOutput::new(aggregated_output), duration, timed_out: false, + os_pid, }; let event_ctx = ToolEventCtx::new(session_ref.as_ref(), turn_ref.as_ref(), &call_id, None); let emitter = ToolEmitter::unified_exec( @@ -202,6 +206,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec( cwd, ExecCommandSource::UnifiedExecStartup, process_id, + os_pid, ); emitter .emit(event_ctx, ToolEventStage::Success(output)) diff --git a/codex-rs/core/src/unified_exec/process.rs b/codex-rs/core/src/unified_exec/process.rs index d52d1a6a9d..b21e1e3446 100644 --- a/codex-rs/core/src/unified_exec/process.rs +++ b/codex-rs/core/src/unified_exec/process.rs @@ -95,6 +95,10 @@ impl UnifiedExecProcess { self.process_handle.output_receiver() } + pub(crate) fn os_pid(&self) -> Option { + self.process_handle.os_pid() + } + pub(super) fn cancellation_token(&self) -> CancellationToken { self.cancellation_token.clone() } @@ -156,6 +160,7 @@ impl UnifiedExecProcess { exit_code, stderr: StreamOutput::new(text.to_string()), aggregated_output: StreamOutput::new(text.to_string()), + os_pid: self.os_pid(), ..Default::default() }; if is_likely_sandbox_denied(sandbox_type, &exec_output) { diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 851b92d24f..0fdb88d439 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -140,6 +140,7 @@ impl UnifiedExecProcessManager { }; let transcript = Arc::new(tokio::sync::Mutex::new(HeadTailBuffer::default())); + let os_pid = process.os_pid(); let event_ctx = ToolEventCtx::new( context.session.as_ref(), context.turn.as_ref(), @@ -151,6 +152,7 @@ impl UnifiedExecProcessManager { cwd.clone(), ExecCommandSource::UnifiedExecStartup, Some(request.process_id.clone()), + os_pid, ); emitter.emit(event_ctx, ToolEventStage::Begin).await; @@ -196,6 +198,7 @@ impl UnifiedExecProcessManager { request.command.clone(), cwd, Some(process_id), + os_pid, Arc::clone(&transcript), output.clone(), exit, @@ -217,6 +220,7 @@ impl UnifiedExecProcessManager { cwd.clone(), start, process_id, + os_pid, Arc::clone(&transcript), ) .await; @@ -401,6 +405,7 @@ impl UnifiedExecProcessManager { cwd: PathBuf, started_at: Instant, process_id: String, + os_pid: Option, transcript: Arc>, ) { let entry = ProcessEntry { @@ -435,6 +440,7 @@ impl UnifiedExecProcessManager { command.to_vec(), cwd, process_id, + os_pid, transcript, started_at, ); diff --git a/codex-rs/core/src/user_shell_command.rs b/codex-rs/core/src/user_shell_command.rs index fb8efcc09c..6e053843fb 100644 --- a/codex-rs/core/src/user_shell_command.rs +++ b/codex-rs/core/src/user_shell_command.rs @@ -89,6 +89,7 @@ mod tests { aggregated_output: StreamOutput::new("hi".to_string()), duration: Duration::from_secs(1), timed_out: false, + os_pid: None, }; let (_, turn_context) = make_session_and_context().await; let item = user_shell_command_record_item("echo hi", &exec_output, &turn_context); @@ -113,6 +114,7 @@ mod tests { aggregated_output: StreamOutput::new("combined output wins".to_string()), duration: Duration::from_millis(120), timed_out: false, + os_pid: None, }; let (_, turn_context) = make_session_and_context().await; let record = format_user_shell_command_record("false", &exec_output, &turn_context); diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index e60fcba8e8..d3856df28e 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -640,6 +640,7 @@ fn exec_command_end_success_produces_completed_command_item() { EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id: "1".to_string(), process_id: None, + os_pid: None, turn_id: "turn-1".to_string(), command: command.clone(), cwd: cwd.clone(), @@ -670,6 +671,7 @@ fn exec_command_end_success_produces_completed_command_item() { EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "1".to_string(), process_id: None, + os_pid: None, turn_id: "turn-1".to_string(), command, cwd, @@ -717,6 +719,7 @@ fn command_execution_output_delta_updates_item_progress() { EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id: "delta-1".to_string(), process_id: Some("42".to_string()), + os_pid: None, turn_id: "turn-1".to_string(), command: command.clone(), cwd: cwd.clone(), @@ -757,6 +760,7 @@ fn command_execution_output_delta_updates_item_progress() { EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "delta-1".to_string(), process_id: Some("42".to_string()), + os_pid: None, turn_id: "turn-1".to_string(), command, cwd, @@ -801,6 +805,7 @@ fn exec_command_end_failure_produces_failed_command_item() { EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id: "2".to_string(), process_id: None, + os_pid: None, turn_id: "turn-1".to_string(), command: command.clone(), cwd: cwd.clone(), @@ -830,6 +835,7 @@ fn exec_command_end_failure_produces_failed_command_item() { EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "2".to_string(), process_id: None, + os_pid: None, turn_id: "turn-1".to_string(), command, cwd, @@ -871,6 +877,7 @@ fn exec_command_end_without_begin_is_ignored() { EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "no-begin".to_string(), process_id: None, + os_pid: None, turn_id: "turn-1".to_string(), command: Vec::new(), cwd: PathBuf::from("."), diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 6850c2ac11..8b2484c748 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1679,6 +1679,10 @@ pub struct ExecCommandBeginEvent { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub process_id: Option, + /// OS process identifier for the spawned command (when available). + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub os_pid: Option, /// Turn ID that this command belongs to. pub turn_id: String, /// The command to be executed. @@ -1703,6 +1707,10 @@ pub struct ExecCommandEndEvent { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub process_id: Option, + /// OS process identifier for the spawned command (when available). + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub os_pid: Option, /// Turn ID that this command belongs to. pub turn_id: String, /// The command that was executed. diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 2d9d808ed1..983b4b9805 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -891,6 +891,7 @@ fn begin_exec_with_source( let event = ExecCommandBeginEvent { call_id: call_id.to_string(), process_id: None, + os_pid: None, turn_id: "turn-1".to_string(), command, cwd, @@ -916,6 +917,7 @@ fn begin_unified_exec_startup( let event = ExecCommandBeginEvent { call_id: call_id.to_string(), process_id: Some(process_id.to_string()), + os_pid: None, turn_id: "turn-1".to_string(), command, cwd, @@ -966,12 +968,14 @@ fn end_exec( source, interaction_input, process_id, + os_pid, } = begin_event; chat.handle_codex_event(Event { id: call_id.clone(), msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, process_id, + os_pid, turn_id, command, cwd, @@ -1244,6 +1248,7 @@ async fn exec_end_without_begin_uses_event_command() { msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "call-orphan".to_string(), process_id: None, + os_pid: None, turn_id: "turn-1".to_string(), command, cwd, @@ -3712,6 +3717,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id: "c1".into(), process_id: None, + os_pid: None, turn_id: "turn-1".into(), command: command.clone(), cwd: cwd.clone(), @@ -3725,6 +3731,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "c1".into(), process_id: None, + os_pid: None, turn_id: "turn-1".into(), command, cwd, diff --git a/codex-rs/tui2/src/chatwidget/tests.rs b/codex-rs/tui2/src/chatwidget/tests.rs index f6cb457bf2..93a6e9893f 100644 --- a/codex-rs/tui2/src/chatwidget/tests.rs +++ b/codex-rs/tui2/src/chatwidget/tests.rs @@ -876,6 +876,7 @@ fn begin_exec_with_source( let event = ExecCommandBeginEvent { call_id: call_id.to_string(), process_id: None, + os_pid: None, turn_id: "turn-1".to_string(), command, cwd, @@ -915,12 +916,14 @@ fn end_exec( source, interaction_input, process_id, + os_pid, } = begin_event; chat.handle_codex_event(Event { id: call_id.clone(), msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, process_id, + os_pid, turn_id, command, cwd, @@ -1196,6 +1199,7 @@ async fn exec_end_without_begin_uses_event_command() { msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "call-orphan".to_string(), process_id: None, + os_pid: None, turn_id: "turn-1".to_string(), command, cwd, @@ -3316,6 +3320,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id: "c1".into(), process_id: None, + os_pid: None, turn_id: "turn-1".into(), command: command.clone(), cwd: cwd.clone(), @@ -3329,6 +3334,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "c1".into(), process_id: None, + os_pid: None, turn_id: "turn-1".into(), command, cwd, diff --git a/codex-rs/utils/pty/src/pipe.rs b/codex-rs/utils/pty/src/pipe.rs index c3dcd4ddcb..bea8dac282 100644 --- a/codex-rs/utils/pty/src/pipe.rs +++ b/codex-rs/utils/pty/src/pipe.rs @@ -209,6 +209,7 @@ pub async fn spawn_process( writer_tx, output_tx, initial_output_rx, + Some(pid), Box::new(PipeChildTerminator { #[cfg(windows)] pid, diff --git a/codex-rs/utils/pty/src/process.rs b/codex-rs/utils/pty/src/process.rs index 5c487fd386..f1fbe04074 100644 --- a/codex-rs/utils/pty/src/process.rs +++ b/codex-rs/utils/pty/src/process.rs @@ -31,6 +31,7 @@ impl fmt::Debug for PtyHandles { pub struct ProcessHandle { writer_tx: mpsc::Sender>, output_tx: broadcast::Sender>, + os_pid: Option, killer: StdMutex>>, reader_handle: StdMutex>>, reader_abort_handles: StdMutex>, @@ -55,6 +56,7 @@ impl ProcessHandle { writer_tx: mpsc::Sender>, output_tx: broadcast::Sender>, initial_output_rx: broadcast::Receiver>, + os_pid: Option, killer: Box, reader_handle: JoinHandle<()>, reader_abort_handles: Vec, @@ -68,6 +70,7 @@ impl ProcessHandle { Self { writer_tx, output_tx, + os_pid, killer: StdMutex::new(Some(killer)), reader_handle: StdMutex::new(Some(reader_handle)), reader_abort_handles: StdMutex::new(reader_abort_handles), @@ -91,6 +94,11 @@ impl ProcessHandle { self.output_tx.subscribe() } + /// Process identifier reported by the underlying OS, if available. + pub fn os_pid(&self) -> Option { + self.os_pid + } + /// True if the child process has exited. pub fn has_exited(&self) -> bool { self.exit_status.load(std::sync::atomic::Ordering::SeqCst) diff --git a/codex-rs/utils/pty/src/pty.rs b/codex-rs/utils/pty/src/pty.rs index 0f1ec2c823..a8e76f221e 100644 --- a/codex-rs/utils/pty/src/pty.rs +++ b/codex-rs/utils/pty/src/pty.rs @@ -86,6 +86,7 @@ pub async fn spawn_process( } let mut child = pair.slave.spawn_command(command_builder)?; + let os_pid = child.process_id(); let killer = child.clone_killer(); let (writer_tx, mut writer_rx) = mpsc::channel::>(128); @@ -156,6 +157,7 @@ pub async fn spawn_process( writer_tx, output_tx, initial_output_rx, + os_pid, Box::new(PtyChildTerminator { killer }), reader_handle, Vec::new(),