Fix skills catalog budgets and warning delivery

This commit is contained in:
jif-oai
2026-06-18 10:37:36 +02:00
parent 3fea72fda6
commit b3d7b2d45a
23 changed files with 309 additions and 218 deletions
+30 -9
View File
@@ -113,7 +113,7 @@ struct AppServerExtensionEventSink {
}
impl ExtensionEventSink for AppServerExtensionEventSink {
fn emit(&self, event: Event) {
fn emit(&self, thread_id: ThreadId, event: Event) {
match event.msg {
EventMsg::ThreadGoalUpdated(thread_goal_event) => {
let thread_id = thread_goal_event.thread_id;
@@ -148,10 +148,20 @@ impl ExtensionEventSink for AppServerExtensionEventSink {
});
}
EventMsg::Warning(warning_event) => {
let Ok(thread_id) = ThreadId::from_string(&event.id) else {
tracing::warn!(event_id = %event.id, "dropping extension warning with invalid thread id");
return;
};
if let Some(listener_command_tx) = self
.thread_state_manager
.current_listener_command_tx(thread_id)
{
let command = ThreadListenerCommand::EmitWarning {
message: warning_event.message.clone(),
};
if listener_command_tx.send(command).is_ok() {
return;
}
tracing::warn!(
"failed to enqueue extension warning for {thread_id}: listener command channel is closed"
);
}
let outgoing = Arc::clone(&self.outgoing);
tokio::spawn(async move {
outgoing
@@ -165,7 +175,7 @@ impl ExtensionEventSink for AppServerExtensionEventSink {
});
}
msg => {
tracing::debug!(event_id = %event.id, ?msg, "dropping unsupported extension event");
tracing::debug!(%thread_id, event_id = %event.id, ?msg, "dropping unsupported extension event");
}
}
}
@@ -203,7 +213,7 @@ mod tests {
use super::*;
#[tokio::test]
async fn app_server_event_sink_uses_listener_fifo_for_goal_updates_and_clears() {
async fn app_server_event_sink_uses_listener_fifo_for_extension_events() {
let (outgoing_tx, _outgoing_rx) = mpsc::channel(4);
let outgoing = Arc::new(OutgoingMessageSender::new(
outgoing_tx,
@@ -216,14 +226,23 @@ mod tests {
let sink = app_server_extension_event_sink(outgoing, thread_state_manager);
for turn_id in ["turn-1", "turn-2"] {
sink.emit(thread_goal_updated_event(thread_id, turn_id));
sink.emit(thread_id, thread_goal_updated_event(thread_id, turn_id));
}
sink.emit(
thread_id,
Event {
id: "turn-2".to_string(),
msg: EventMsg::Warning(codex_protocol::protocol::WarningEvent {
message: "skill warning".to_string(),
}),
},
);
listener_command_tx
.send(ThreadListenerCommand::EmitThreadGoalCleared)
.expect("listener command channel should be open");
let mut observed = Vec::new();
for _ in 0..3 {
for _ in 0..4 {
let command = timeout(Duration::from_secs(1), listener_command_rx.recv())
.await
.expect("timed out waiting for listener command")
@@ -235,6 +254,7 @@ mod tests {
ThreadListenerCommand::EmitThreadGoalCleared => {
observed.push("cleared".to_string())
}
ThreadListenerCommand::EmitWarning { message } => observed.push(message),
_ => panic!("unexpected listener command"),
}
}
@@ -243,6 +263,7 @@ mod tests {
vec![
"turn-1".to_string(),
"turn-2".to_string(),
"skill warning".to_string(),
"cleared".to_string()
],
observed
@@ -276,6 +276,7 @@ use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::TurnSteerResponse;
use codex_app_server_protocol::UserInput as V2UserInput;
use codex_app_server_protocol::WarningNotification;
use codex_app_server_protocol::WindowsSandboxReadiness;
use codex_app_server_protocol::WindowsSandboxReadinessResponse;
use codex_app_server_protocol::WindowsSandboxSetupCompletedNotification;
@@ -505,6 +505,14 @@ pub(super) async fn handle_thread_listener_command(
ThreadListenerCommand::EmitThreadGoalSnapshot { state_db } => {
send_thread_goal_snapshot_notification(outgoing, conversation_id, &state_db).await;
}
ThreadListenerCommand::EmitWarning { message } => {
outgoing
.send_server_notification(ServerNotification::Warning(WarningNotification {
thread_id: Some(conversation_id.to_string()),
message,
}))
.await;
}
ThreadListenerCommand::ResolveServerRequest {
request_id,
completion_tx,
+4
View File
@@ -56,6 +56,10 @@ pub(crate) enum ThreadListenerCommand {
EmitThreadGoalSnapshot {
state_db: StateDbHandle,
},
// EmitWarning is used to order extension warnings with core turn events.
EmitWarning {
message: String,
},
// ResolveServerRequest is used to notify the client that the request has been resolved.
// It is executed in the thread listener's context to ensure that the resolved notification is ordered with regard to the request itself.
ResolveServerRequest {