Skip to content

Commit f3e5d2e

Browse files
authored
Merge pull request #34 from helixml/fix/stopped-flush-turn-scoped-request-id
Fix Stopped flush: turn-scoped request_id, only flush current turn
2 parents 3474a91 + 255f6b4 commit f3e5d2e

1 file changed

Lines changed: 31 additions & 7 deletions

File tree

crates/external_websocket_sync/src/thread_service.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,14 @@ fn ensure_thread_subscription(
436436
let thread_id_for_sub = thread_id.to_string();
437437
mark_persistent_subscription(thread_id.to_string());
438438

439+
// Track the request_id that was active when the current turn started.
440+
// Used by the Stopped handler to flush with the correct request_id,
441+
// even if a follow-up/interrupt message has already updated the global
442+
// THREAD_REQUEST_MAP to the next turn's request_id.
443+
let turn_request_id: std::cell::RefCell<String> = std::cell::RefCell::new(
444+
crate::get_thread_request_id(thread_id).unwrap_or_default()
445+
);
446+
439447
cx.subscribe(thread_entity, move |thread_entity, event, cx| {
440448
match event {
441449
AcpThreadEvent::NewEntry => {
@@ -465,6 +473,12 @@ fn ensure_thread_subscription(
465473
};
466474
let rid = crate::get_thread_request_id(&thread_id_for_sub)
467475
.unwrap_or_default();
476+
// Snapshot the request_id when the first assistant entry of a turn appears.
477+
// This ensures the Stopped flush uses the turn's own request_id, not a
478+
// later follow-up's.
479+
if role == "assistant" {
480+
*turn_request_id.borrow_mut() = rid.clone();
481+
}
468482
let _ = crate::send_websocket_event(SyncEvent::MessageAdded {
469483
acp_thread_id: thread_id_for_sub.clone(),
470484
message_id: latest_idx.to_string(),
@@ -517,9 +531,19 @@ fn ensure_thread_subscription(
517531
// content is safe: the Go accumulator uses overwrite semantics for known message_ids.
518532
let thread = thread_entity.read(cx);
519533
let entries = thread.entries();
520-
let rid = crate::get_thread_request_id(&thread_id_for_sub)
521-
.unwrap_or_default();
522-
for (idx, entry) in entries.iter().enumerate() {
534+
// Use the request_id captured when this turn started, NOT the current
535+
// global request_id. If a follow-up/interrupt message arrived before
536+
// Stopped fires, the global ID already points to the next turn.
537+
let rid = turn_request_id.borrow().clone();
538+
539+
// Find the start of the current turn: the entry AFTER the last UserMessage.
540+
// Only flush entries from the current turn — sending old entries would cause
541+
// them to leak into the current interaction's response_entries on the Go side.
542+
let turn_start = entries.iter().enumerate().rev()
543+
.find_map(|(i, e)| matches!(e, acp_thread::AgentThreadEntry::UserMessage(_)).then_some(i + 1))
544+
.unwrap_or(0);
545+
546+
for (idx, entry) in entries.iter().enumerate().skip(turn_start) {
523547
match entry {
524548
acp_thread::AgentThreadEntry::AssistantMessage(msg) => {
525549
let content = msg.content_only(cx);
@@ -559,16 +583,16 @@ fn ensure_thread_subscription(
559583
}
560584
}
561585

562-
let rid = crate::get_thread_request_id(&thread_id_for_sub)
563-
.unwrap_or_default();
586+
// Use the turn's captured request_id for message_completed too
587+
let completed_rid = turn_request_id.borrow().clone();
564588
eprintln!(
565589
"📤 [THREAD_SERVICE] Stopped event: sending message_completed for thread {} (request_id={})",
566-
thread_id_for_sub, rid
590+
thread_id_for_sub, completed_rid
567591
);
568592
let _ = crate::send_websocket_event(SyncEvent::MessageCompleted {
569593
acp_thread_id: thread_id_for_sub.clone(),
570594
message_id: "0".to_string(),
571-
request_id: rid,
595+
request_id: completed_rid,
572596
});
573597
}
574598
_ => {}

0 commit comments

Comments
 (0)