umanggarg Claude Sonnet 4.6 commited on
Commit
60ca7ef
Β·
1 Parent(s): 019b48f

fix: mode tag correctly shows RAG/Agent, fix streaming ID collision

Browse files

Bug: 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>

Files changed (2) hide show
  1. ui/src/App.jsx +12 -1
  2. ui/src/components/Message.jsx +4 -6
ui/src/App.jsx CHANGED
@@ -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
- const assistantId = Date.now();
 
 
 
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 {
ui/src/components/Message.jsx CHANGED
@@ -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
- "phase" is the reliable mode signal: null = agent, string = RAG (set at message
362
- creation in App.jsx). Compact label: "✦ Agent Β· 4 steps" or "β—Ž RAG Β· hybrid". */}
363
- {"phase" in msg && (
364
  <div className="msg-mode-tag">
365
- {msg.phase === null ? (
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>