Skip to content
Merged
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
27 changes: 26 additions & 1 deletion crates/client/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,14 @@ impl AgentOs {
max_events: Option<usize>,
max_bytes: Option<usize>,
) -> std::result::Result<ProcessOutputReplay, ClientError> {
let (process_id, retain_output, execution_id, execution_generation) = self
let (
process_id,
retain_output,
execution_id,
execution_generation,
mut started,
mut outcome,
) = self
.inner()
.processes
.read(&pid, |_, entry| {
Expand All @@ -1009,9 +1016,27 @@ impl AgentOs {
entry.retain_output,
entry.execution_id.clone(),
entry.execution_generation,
entry.kernel_pid.subscribe(),
entry.exit_tx.subscribe(),
)
})
.ok_or(ClientError::ProcessNotFound(pid))?;
// `spawn_process` returns before the sidecar has the process. Wait until the Execute
// request lands or fails, so a read never reaches the sidecar before the process exists.
loop {
Comment on lines +1025 to +1026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Medium · Track spawn acknowledgement separately from the optional kernel PID

kernel_pid is not a reliable acknowledgement flag: ProcessStartedResponse.pid is explicitly optional, and run_spawn accepts a successful response while only updating this watch channel for Some(pid). With a valid process_started response containing no PID, a long-running process leaves both started as None and outcome as Pending, so this call waits indefinitely even though replay is already available. Add a dedicated spawn-result/readiness watch (as the shell path does), and signal it on every successful send_execute response rather than overloading the optional PID mapping.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not reachable. slop

Comment thread
eersnington marked this conversation as resolved.
if started.borrow().is_some() {
break;
}
match &*outcome.borrow() {
ProcessOutcome::Failed { error, .. } => return Err(error.clone()),
ProcessOutcome::Exited(_) => break,
ProcessOutcome::Pending => {}
}
tokio::select! {
changed = started.changed() => if changed.is_err() { break },
changed = outcome.changed() => if changed.is_err() { break },
}
}
if !retain_output {
return Err(ClientError::Sidecar(format!(
"process {pid} was not spawned with output retention enabled"
Expand Down
34 changes: 34 additions & 0 deletions crates/client/tests/process_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,40 @@ async fn spawn_rejection_and_fast_exit_remain_distinct_for_late_waiters() {
.expect("process outcome parity");
}

#[tokio::test]
async fn output_read_right_after_a_failed_spawn_returns_the_launch_error() {
if !common::require_sidecar("output_read_right_after_a_failed_spawn_returns_the_launch_error") {
return;
}
let os = common::new_vm().await;
let result = tokio::time::timeout(std::time::Duration::from_secs(20), async {
let launch = os.spawn_process(
"agentos-nonexistent-review-command",
Vec::new(),
SpawnOptions {
retain_output: true,
..Default::default()
},
)?;
// Read before the launch has settled: the caller must get the launch error, not a
// missing-process error from asking the sidecar before it knows the process.
let read = os.read_process_output(launch.pid, None, None, None).await;
anyhow::ensure!(
matches!(&read, Err(ClientError::Kernel { code, .. }) if code == "ENOENT"),
"an output read right after a failed spawn must return the launch error: {read:?}"
);
Ok::<_, anyhow::Error>(())
})
.await;
tokio::time::timeout(std::time::Duration::from_secs(10), os.shutdown())
.await
.expect("bounded VM cleanup")
.expect("shutdown VM");
result
.expect("output read finishes within the timeout")
.expect("output read returns the launch error");
}

#[tokio::test]
async fn exec_argv_timeout_confirms_node_guest_exit() {
if !common::require_sidecar("exec_argv_timeout_confirms_node_guest_exit") {
Expand Down
Loading