diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs index a2465bab7..9f5adf8f9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs @@ -152,6 +152,12 @@ impl DirectThreadManager { cleanable, }); Self::mark_item_events_cleanable(thread, event.item_id.as_deref()); + if matches!( + event.event_type.as_str(), + "approval.resolved" | "request.resolved" | "ask.resolved" + ) { + Self::mark_request_events_cleanable(thread, request_id(&event).as_deref()); + } self.evict(thread_id); event } @@ -266,10 +272,11 @@ impl DirectThreadManager { true } "approval.requested" | "request.requested" | "ask.requested" => { - if let Some(request_id) = request_id(event) { - thread.unresolved_requests.insert(request_id); + let request_id = request_id(event); + if let Some(request_id) = request_id.as_deref() { + thread.unresolved_requests.insert(request_id.to_string()); } - false + request_id.is_none() } "approval.resolved" | "request.resolved" | "ask.resolved" => { if let Some(request_id) = request_id(event) { @@ -316,6 +323,21 @@ impl DirectThreadManager { } } + fn mark_request_events_cleanable(thread: &mut ThreadState, resolved_request_id: Option<&str>) { + let Some(resolved_request_id) = resolved_request_id else { + return; + }; + for stored in &mut thread.events { + if matches!( + stored.event.event_type.as_str(), + "approval.requested" | "request.requested" | "ask.requested" + ) && request_id(&stored.event).as_deref() == Some(resolved_request_id) + { + stored.cleanable = true; + } + } + } + fn trim_prefix(thread: &mut ThreadState) { let min_cursor = thread .subscribers @@ -583,6 +605,34 @@ mod tests { ); } + #[test] + fn resolved_request_releases_the_original_requested_event() { + let mut manager = DirectThreadManager::with_limits(100, 100_000); + manager.append( + "thread-1", + DirectThreadRawEventDraft { + event_type: "approval.requested".to_string(), + turn_id: "turn-1".to_string(), + item_id: None, + payload: serde_json::json!({"requestId": "request-1"}), + }, + ); + let subscription = manager.subscribe("thread-1"); + manager.append( + "thread-1", + DirectThreadRawEventDraft { + event_type: "approval.resolved".to_string(), + turn_id: "turn-1".to_string(), + item_id: None, + payload: serde_json::json!({"requestId": "request-1"}), + }, + ); + manager + .consume(&subscription.subscription_id) + .expect("consume resolution"); + assert_eq!(manager.thread_debug("thread-1").unwrap().0, 0); + } + #[test] fn turn_completed_anchor_survives_empty_queue_for_new_subscriber() { let mut manager = DirectThreadManager::with_limits(100, 100_000);