fix: preserve terminal turn failures

This commit is contained in:
2026-05-30 09:02:11 +09:00
parent e37c151f0e
commit a47f2c4689
4 changed files with 107 additions and 12 deletions
+5
View File
@@ -617,6 +617,11 @@ async fn controller_loop<C, St>(
// here so the status flip → drive_turn → finish sequence lives
// in one place, regardless of which Method caused it.
if let Some(run) = pending.take() {
// Cancellation is meaningful only for an accepted running turn. Clear
// idle/stale signals before the status flip; any Cancel/Pause received
// after this point is delivered to the turn and must not be discarded by
// the Worker at run start.
pod.worker_mut().clear_pending_cancel();
set_controller_status(&shared_state, &runtime_dir, &event_tx, PodStatus::Running).await;
let parent_originated = run.is_parent_originated();
let (new_status, shutdown) = match run {
+47 -1
View File
@@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use futures::{Stream, StreamExt};
use llm_worker::Worker;
use llm_worker::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent};
use llm_worker::llm_client::event::{ErrorEvent, Event as LlmEvent, ResponseStatus, StatusEvent};
use llm_worker::llm_client::types::Item;
use llm_worker::llm_client::{ClientError, LlmClient, Request};
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
@@ -254,6 +254,52 @@ async fn run_end_returns_to_idle_without_busy_status() {
assert_eq!(handle.shared_state.get_status(), PodStatus::Idle);
}
#[tokio::test]
async fn provider_stream_error_records_run_errored() {
let client = MockClient::new(vec![LlmEvent::Error(ErrorEvent {
code: Some("context_length_exceeded".into()),
message: "request too large".into(),
})]);
let pod = make_pod(client).await;
let handle = spawn_controller(pod).await;
let mut rx = handle.subscribe();
handle.send(Method::run_text("ping")).await.unwrap();
assert!(
drain_until(&mut rx, std::time::Duration::from_secs(2), |e| matches!(
e,
Event::Error {
code: protocol::ErrorCode::ProviderError,
message,
} if message.contains("context_length_exceeded")
))
.await,
"provider stream error should be surfaced as a live provider error"
);
wait_for_status(&handle, PodStatus::Idle).await;
let (entries, _rx) = handle.sink.subscribe_with_snapshot();
assert!(
entries.iter().any(|entry| matches!(
entry,
LogEntry::RunErrored { message, .. }
if message.contains("context_length_exceeded")
)),
"provider stream error should be persisted as RunErrored"
);
assert!(
!entries.iter().any(|entry| matches!(
entry,
LogEntry::RunCompleted {
result: llm_worker::WorkerResult::Finished,
..
}
)),
"provider stream error must not be recorded as a finished run"
);
}
/// Mid-turn re-attach: a client connecting while the worker is still
/// running observes the in-flight `UserInput` entry in the connect-time
/// `Event::Snapshot`. This is the load-bearing property of the new