app-server: Silence thread status changes caused by thread being created (#13079)

Currently we emit `thread/status/changed` with `Idle` status right
before sending `thread/started` event (which also has `Idle` status in
it).
It feels that there is no point in that as client has no way to know
prior state of the thread as it didn't exist yet, so silence these kinds
of notifications.
This commit is contained in:
Ruslan Nigmatullin
2026-03-02 16:52:28 -08:00
committed by GitHub
parent 146b798129
commit 9022cdc563
6 changed files with 143 additions and 21 deletions

View File

@@ -91,7 +91,12 @@ impl ThreadWatchManager {
}
pub(crate) async fn upsert_thread(&self, thread: Thread) {
self.mutate_and_publish(move |state| state.upsert_thread(thread.id))
self.mutate_and_publish(move |state| state.upsert_thread(thread.id, true))
.await;
}
pub(crate) async fn upsert_thread_silently(&self, thread: Thread) {
self.mutate_and_publish(move |state| state.upsert_thread(thread.id, false))
.await;
}
@@ -289,14 +294,22 @@ struct ThreadWatchState {
}
impl ThreadWatchState {
fn upsert_thread(&mut self, thread_id: String) -> Option<ThreadStatusChangedNotification> {
fn upsert_thread(
&mut self,
thread_id: String,
emit_notification: bool,
) -> Option<ThreadStatusChangedNotification> {
let previous_status = self.status_for(&thread_id);
let runtime = self
.runtime_by_thread_id
.entry(thread_id.clone())
.or_default();
runtime.is_loaded = true;
self.status_changed_notification(thread_id, previous_status)
if emit_notification {
self.status_changed_notification(thread_id, previous_status)
} else {
None
}
}
fn remove_thread(&mut self, thread_id: &str) -> Option<ThreadStatusChangedNotification> {
@@ -692,6 +705,45 @@ mod tests {
);
}
#[tokio::test]
async fn silent_upsert_skips_initial_notification() {
let (outgoing_tx, mut outgoing_rx) = mpsc::channel(8);
let manager = ThreadWatchManager::new_with_outgoing(Arc::new(OutgoingMessageSender::new(
outgoing_tx,
)));
manager
.upsert_thread_silently(test_thread(
INTERACTIVE_THREAD_ID,
codex_app_server_protocol::SessionSource::Cli,
))
.await;
assert_eq!(
manager
.loaded_status_for_thread(INTERACTIVE_THREAD_ID)
.await,
ThreadStatus::Idle,
);
assert!(
timeout(Duration::from_millis(100), outgoing_rx.recv())
.await
.is_err(),
"silent upsert should not emit thread/status/changed"
);
manager.note_turn_started(INTERACTIVE_THREAD_ID).await;
assert_eq!(
recv_status_changed_notification(&mut outgoing_rx).await,
ThreadStatusChangedNotification {
thread_id: INTERACTIVE_THREAD_ID.to_string(),
status: ThreadStatus::Active {
active_flags: vec![],
},
},
);
}
async fn wait_for_status(
manager: &ThreadWatchManager,
thread_id: &str,