Preserve queued output for observers during code-mode termination (#48207)

## Why

Yield signals and queued runtime events can detach an observer during termination, before the remaining output has been drained.

## What changed

Track the active yield signal separately from the observer so termination can disable yielding while keeping the observer attached. Ignore `Started`, `Pending`, and `YieldRequested` events during termination, allowing queued output to reach the observer in `CellEvent::Terminated`.

## Testing

Extend the termination regression test with an immediate yield timeout, a canceled yield signal, and queued start, yield, output, and completion events. Assert that both the termination caller and the initial observer receive the queued output in the terminated event.

GitOrigin-RevId: 1c08bdec06838123aaa102268da2f50b63ddea3f
This commit is contained in:
pakrym-oai
2026-09-25 20:34:52 +00:00
committed by copyberry
parent 4193f1acfb
commit f5ffa46959
2 changed files with 34 additions and 5 deletions
@@ -89,6 +89,7 @@ impl CellHost for RecordingHost {
struct CellActorHarness {
event_tx: mpsc::UnboundedSender<RuntimeEvent>,
handle: CellHandle,
yield_signal: CancellationToken,
initial_event_rx: oneshot::Receiver<Result<CellEvent, CellError>>,
task: tokio::task::JoinHandle<()>,
runtime_control_rx: std_mpsc::Receiver<RuntimeControlCommand>,
@@ -136,6 +137,7 @@ fn spawn_cell_actor_harness_with_host_and_failure_handler<H: CellHost>(
let (runtime_control_tx, runtime_control_rx) = std_mpsc::channel();
let cell_state = Arc::new(CellState::new(CancellationToken::new()));
let handle = CellHandle::new(command_tx, Arc::clone(&cell_state));
let yield_signal = CancellationToken::new();
let task = tokio::spawn(run_cell(
host,
CellContext {
@@ -147,7 +149,7 @@ fn spawn_cell_actor_harness_with_host_and_failure_handler<H: CellHost>(
event_rx,
command_rx,
Observer {
observation: initial_observe_mode.into(),
observation: initial_observe_mode.with_yield_signal(yield_signal.clone()),
response_tx: initial_event_tx,
},
task_failure_handler,
@@ -156,6 +158,7 @@ fn spawn_cell_actor_harness_with_host_and_failure_handler<H: CellHost>(
CellActorHarness {
event_tx,
handle,
yield_signal,
initial_event_rx,
task,
runtime_control_rx,
@@ -256,7 +259,17 @@ async fn yield_timer_preempts_buffered_runtime_output() {
#[tokio::test]
async fn queued_termination_preempts_unobserved_runtime_completion() {
let harness = spawn_cell_actor_harness(ObserveMode::YieldAfter(Duration::from_secs(60)));
let harness = spawn_cell_actor_harness(ObserveMode::YieldAfter(Duration::ZERO));
harness.event_tx.send(RuntimeEvent::Started).unwrap();
harness.event_tx.send(RuntimeEvent::YieldRequested).unwrap();
harness
.event_tx
.send(RuntimeEvent::ContentItem(
FunctionCallOutputContentItem::InputText {
text: "queued output".to_string(),
},
))
.unwrap();
harness
.event_tx
.send(RuntimeEvent::Result {
@@ -264,10 +277,13 @@ async fn queued_termination_preempts_unobserved_runtime_completion() {
error_text: None,
})
.unwrap();
harness.yield_signal.cancel();
let termination = harness.handle.terminate();
let terminated = Ok(CellEvent::Terminated {
content_items: Vec::new(),
content_items: vec![OutputItem::Text {
text: "queued output".to_string(),
}],
});
assert_eq!(termination.await, terminated.clone());
assert_eq!(harness.initial_event_rx.await.unwrap(), terminated);