From fad4272754b1b9377f64d63045c28e91f6d9a2c1 Mon Sep 17 00:00:00 2001 From: "ilblackdragon@gmail.com" Date: Thu, 26 Mar 2026 22:46:36 -0700 Subject: [PATCH] fix(engine): route messages to correct conversation by thread scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Messages sent from a new conversation in the gateway always appeared in the default assistant conversation because handle_with_engine ignored the thread_id from the frontend. Two fixes: 1. Engine conversation scoping — when the message carries a thread_id (from the frontend's conversation picker), use it as part of the engine conversation key: "gateway:" instead of just "gateway". This creates a distinct engine conversation per v1 thread, so messages don't cross-contaminate. 2. V1 dual-write targeting — write user messages and assistant responses to the v1 conversation matching the thread_id (via ensure_conversation), not the hardcoded assistant conversation. Falls back to the assistant conversation when no thread_id is present (e.g., default chat). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/bridge/router.rs | 65 +++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/src/bridge/router.rs b/src/bridge/router.rs index d5e8dd25..a359157b 100644 --- a/src/bridge/router.rs +++ b/src/bridge/router.rs @@ -790,10 +790,21 @@ pub async fn handle_with_engine( // Reset the per-step call counter so each thread starts fresh state.effect_adapter.reset_call_count(); - // Get or create conversation for this channel+user + // Scope the engine conversation by (channel, user, thread). + // When the frontend sends a thread_id (user created a new conversation), + // use it as part of the channel key so each v1 thread maps to a distinct + // engine conversation. Without this, all threads share one conversation + // and messages appear in the wrong place. + let scope = message.conversation_scope(); + let channel_key = match scope { + Some(tid) => format!("{}:{}", message.channel, tid), + None => message.channel.clone(), + }; + + // Get or create conversation for this scoped channel+user let conv_id = state .conversation_manager - .get_or_create_conversation(&message.channel, &message.user_id) + .get_or_create_conversation(&channel_key, &message.user_id) .await .map_err(|e| engine_err("conversation error", e))?; @@ -813,14 +824,26 @@ pub async fn handle_with_engine( .await .map_err(|e| engine_err("thread error", e))?; - if let Some(ref db) = state.db - && let Ok(conv_id_v1) = db - .get_or_create_assistant_conversation(&message.user_id, &message.channel) - .await - { - let _ = db - .add_conversation_message(conv_id_v1, "user", content) - .await; + // Dual-write to v1 database so the gateway history API shows messages. + // Use the thread-scoped conversation (from thread_id) when available, + // falling back to the default assistant conversation. + if let Some(ref db) = state.db { + let v1_conv_id = if let Some(tid) = scope + && let Ok(uuid) = uuid::Uuid::parse_str(tid) + { + // Ensure the v1 conversation exists for this thread + let _ = db + .ensure_conversation(uuid, &message.channel, &message.user_id, Some(tid)) + .await; + Some(uuid) + } else { + db.get_or_create_assistant_conversation(&message.user_id, &message.channel) + .await + .ok() + }; + if let Some(cid) = v1_conv_id { + let _ = db.add_conversation_message(cid, "user", content).await; + } } debug!(thread_id = %thread_id, "engine v2: thread spawned"); @@ -878,16 +901,26 @@ async fn await_thread_outcome( .map_err(|e| engine_err("conversation error", e))?; if let Some(ref db) = state.db - && let Ok(conv_id_v1) = db - .get_or_create_assistant_conversation(&message.user_id, &message.channel) - .await && let ThreadOutcome::Completed { response: Some(ref text), } = outcome { - let _ = db - .add_conversation_message(conv_id_v1, "assistant", text) - .await; + // Write response to the correct v1 conversation (thread-scoped or assistant) + let scope = message.conversation_scope(); + let v1_conv_id = if let Some(tid) = scope + && let Ok(uuid) = uuid::Uuid::parse_str(tid) + { + Some(uuid) + } else { + db.get_or_create_assistant_conversation(&message.user_id, &message.channel) + .await + .ok() + }; + if let Some(cid) = v1_conv_id { + let _ = db + .add_conversation_message(cid, "assistant", text) + .await; + } } if let Some(ref sse) = state.sse