Spaces:
Running
fix: mode tag correctly shows RAG/Agent, fix streaming ID collision
Browse filesBug: mode tag showed "β¦ AGENT" on RAG responses.
Root cause 1: assistantId = Date.now() β if rate-limit auto-retry fires and
handleSubmit runs at the exact same millisecond, the new message gets the
same ID as the old one. The stale RAG onSources callback then writes queryType
onto the new agent message (same ID), while phase=null stays agent. Result:
AGENT tag + RAG pipeline bar on the same message.
Root cause 2: mode was inferred from msg.phase (null=agent, string=RAG), which
is mutable β any async callback could flip it.
Fix 1: Replace Date.now() with a monotonic msgIdCounter.current (++counter),
guaranteeing unique IDs regardless of timing.
Fix 2: Stop the old stream before auto-retrying, preventing stale callbacks
from firing on the new message at all.
Fix 3: Add explicit msg.mode = "agent"|"rag" set at creation, never mutated.
Message.jsx reads msg.mode instead of inferring from phase.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ui/src/App.jsx +12 -1
- ui/src/components/Message.jsx +4 -6
|
@@ -47,6 +47,7 @@ export default function App() {
|
|
| 47 |
const streamingRef = useRef(false); // always-fresh streaming flag for event handlers
|
| 48 |
const countdownTimer = useRef(null); // setInterval handle for rate-limit auto-retry
|
| 49 |
const handleSubmitRef = useRef(null); // stable ref so closures can call handleSubmit
|
|
|
|
| 50 |
const rateLimitRetries = useRef(0); // consecutive rate-limit count β resets on success
|
| 51 |
|
| 52 |
// ββ Multi-session persistence (localStorage, up to 10 sessions per repo) βββ
|
|
@@ -329,9 +330,15 @@ export default function App() {
|
|
| 329 |
// On auto-retry (retryQuestion set), skip the user message β it's already in the chat
|
| 330 |
// from the first attempt. Adding it again causes duplicate question bubbles.
|
| 331 |
const userMsg = { role: "user", content: question };
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
| 333 |
const assistantMsg = {
|
| 334 |
id: assistantId, role: "assistant",
|
|
|
|
|
|
|
|
|
|
| 335 |
content: "", sources: [], queryType: null, streaming: true,
|
| 336 |
phase: agentMode ? null : "searching",
|
| 337 |
sourceCount: null,
|
|
@@ -405,6 +412,10 @@ export default function App() {
|
|
| 405 |
if (secsLeft <= 0) {
|
| 406 |
clearInterval(countdownTimer.current);
|
| 407 |
countdownTimer.current = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
setMessages(prev => prev.filter(m => m.id !== assistantId));
|
| 409 |
handleSubmitRef.current?.(question);
|
| 410 |
} else {
|
|
|
|
| 47 |
const streamingRef = useRef(false); // always-fresh streaming flag for event handlers
|
| 48 |
const countdownTimer = useRef(null); // setInterval handle for rate-limit auto-retry
|
| 49 |
const handleSubmitRef = useRef(null); // stable ref so closures can call handleSubmit
|
| 50 |
+
const msgIdCounter = useRef(0); // monotonic counter for message IDs β avoids Date.now() collisions
|
| 51 |
const rateLimitRetries = useRef(0); // consecutive rate-limit count β resets on success
|
| 52 |
|
| 53 |
// ββ Multi-session persistence (localStorage, up to 10 sessions per repo) βββ
|
|
|
|
| 330 |
// On auto-retry (retryQuestion set), skip the user message β it's already in the chat
|
| 331 |
// from the first attempt. Adding it again causes duplicate question bubbles.
|
| 332 |
const userMsg = { role: "user", content: question };
|
| 333 |
+
// Use a unique counter (not Date.now()) so auto-retry can never create a
|
| 334 |
+
// new message with the same ID as the old one β preventing a stale onSources
|
| 335 |
+
// callback from the old RAG stream polluting the new message's state.
|
| 336 |
+
const assistantId = ++msgIdCounter.current;
|
| 337 |
const assistantMsg = {
|
| 338 |
id: assistantId, role: "assistant",
|
| 339 |
+
// Store mode explicitly so Message.jsx never has to infer it from mutable state.
|
| 340 |
+
// phase, queryType, etc. can all be overwritten by async callbacks; mode cannot.
|
| 341 |
+
mode: agentMode ? "agent" : "rag",
|
| 342 |
content: "", sources: [], queryType: null, streaming: true,
|
| 343 |
phase: agentMode ? null : "searching",
|
| 344 |
sourceCount: null,
|
|
|
|
| 412 |
if (secsLeft <= 0) {
|
| 413 |
clearInterval(countdownTimer.current);
|
| 414 |
countdownTimer.current = null;
|
| 415 |
+
// Stop the old stream before retrying β prevents stale onSources/onToken
|
| 416 |
+
// callbacks from the previous attempt firing on the new message.
|
| 417 |
+
stopStream.current?.();
|
| 418 |
+
stopStream.current = null;
|
| 419 |
setMessages(prev => prev.filter(m => m.id !== assistantId));
|
| 420 |
handleSubmitRef.current?.(question);
|
| 421 |
} else {
|
|
@@ -358,12 +358,11 @@ const Message = forwardRef(function Message({ msg, onDiagramThis, onRetry, showR
|
|
| 358 |
{/* All assistant content in a column wrapper */}
|
| 359 |
<div className="message-content">
|
| 360 |
{/* Mode tag β always shown on assistant responses so chat history is scannable.
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
{
|
| 364 |
<div className="msg-mode-tag">
|
| 365 |
-
{msg.
|
| 366 |
-
// Agent mode: phase is explicitly null (set before any tools run)
|
| 367 |
<>
|
| 368 |
<span className="msg-mode-icon">β¦</span>
|
| 369 |
<span className="msg-mode-label">Agent</span>
|
|
@@ -372,7 +371,6 @@ const Message = forwardRef(function Message({ msg, onDiagramThis, onRetry, showR
|
|
| 372 |
)}
|
| 373 |
</>
|
| 374 |
) : (
|
| 375 |
-
// RAG mode: phase = "searching" β "generating" β done
|
| 376 |
<>
|
| 377 |
<span className="msg-mode-icon">β</span>
|
| 378 |
<span className="msg-mode-label">RAG</span>
|
|
|
|
| 358 |
{/* All assistant content in a column wrapper */}
|
| 359 |
<div className="message-content">
|
| 360 |
{/* Mode tag β always shown on assistant responses so chat history is scannable.
|
| 361 |
+
Uses msg.mode (set explicitly at creation, never mutated) so async callbacks
|
| 362 |
+
updating queryType/phase/model can never flip the label to the wrong mode. */}
|
| 363 |
+
{msg.mode && (
|
| 364 |
<div className="msg-mode-tag">
|
| 365 |
+
{msg.mode === "agent" ? (
|
|
|
|
| 366 |
<>
|
| 367 |
<span className="msg-mode-icon">β¦</span>
|
| 368 |
<span className="msg-mode-label">Agent</span>
|
|
|
|
| 371 |
)}
|
| 372 |
</>
|
| 373 |
) : (
|
|
|
|
| 374 |
<>
|
| 375 |
<span className="msg-mode-icon">β</span>
|
| 376 |
<span className="msg-mode-label">RAG</span>
|