fix(frontend): resolve agent response buffering due to stale EventSou…#991
Conversation
WalkthroughReplaced direct Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/frontend/src/hooks/use-agui-stream.ts (1)
133-141: 🛠️ Refactor suggestion | 🟠 MajorRemove
processEventfromconnect's dependency array.Since the handler now accesses
processEventthrough the ref rather than capturing it directly,processEventshould be removed from the dependency array at line 188. Keeping it causes unnecessary recreations ofconnectwheneverprocessEventchanges, which defeats part of the purpose of using the ref pattern.Proposed fix
}, - [projectName, sessionName, processEvent, onConnected, onError, onDisconnected], + [projectName, sessionName, onConnected, onError, onDisconnected], )Also applies to: 188-188
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/frontend/src/hooks/use-agui-stream.ts` around lines 133 - 141, The connect function's dependency array still includes processEvent even though the SSE handler uses processEventRef.current, causing unnecessary recreations; remove processEvent from the dependency array of connect in use-agui-stream.ts (leave other deps intact), and ensure processEventRef is kept in sync elsewhere (the existing ref assignment to processEventRef.current should remain) so the handler continues to call the latest processEvent via processEventRef.current.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@components/frontend/src/hooks/use-agui-stream.ts`:
- Around line 133-141: The connect function's dependency array still includes
processEvent even though the SSE handler uses processEventRef.current, causing
unnecessary recreations; remove processEvent from the dependency array of
connect in use-agui-stream.ts (leave other deps intact), and ensure
processEventRef is kept in sync elsewhere (the existing ref assignment to
processEventRef.current should remain) so the handler continues to call the
latest processEvent via processEventRef.current.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 6d39a692-ce50-4045-8cc2-5d38f907590c
📒 Files selected for processing (1)
components/frontend/src/hooks/use-agui-stream.ts
|
@psav can u look at the code rabbit comment. is it accurate
|
|
I rechecked the current head, and these points are already addressed:
So my earlier concern was accurate for an earlier revision, but it’s stale against the current PR state. |
…rce closure Fixes intermittent message buffering where agent responses don't display until the user sends another message. The EventSource onmessage handler was capturing a stale processEvent closure, causing events to be processed with outdated setState callbacks when component callbacks changed (e.g., during agent team operations). Use a ref to ensure the handler always calls the latest processEvent, guaranteeing immediate UI updates. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Complete the ref pattern optimization by removing processEvent from the connect callback's dependency array. Since the EventSource handler now accesses processEvent through processEventRef.current, including it in dependencies causes unnecessary reconnections when processEvent changes, defeating the purpose of the ref pattern. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
27e55a7 to
e1f5bbc
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/frontend/src/hooks/use-agui-stream.ts`:
- Around line 48-49: Replace the async ref update with a synchronous one:
initialize processEventRef as a non-null no-op (useRef<(event: PlatformEvent) =>
void>(() => {})) and change the effect that assigns processEventRef.current =
processEvent from useEffect to useLayoutEffect so the SSE handler
(processEventRef.current?.(event)) sees the updated callback immediately on
mount; keep the EventSource handler logic unchanged but ensure it calls
processEventRef.current(event).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 8eb185d7-4603-4572-877e-5ca19e6f48de
📒 Files selected for processing (1)
components/frontend/src/hooks/use-agui-stream.ts
| // Store latest processEvent to avoid stale closure in EventSource handler | ||
| const processEventRef = useRef<((event: PlatformEvent) => void) | null>(null) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In React 19 client components, does useEffectrun after paint whileuseLayoutEffectruns synchronously after commit, and isuseLayoutEffect the recommended pattern for keeping callback refs fresh for external event listeners (like EventSource)?
💡 Result:
Yes, in React 19 client components, useEffect runs asynchronously after paint (or before paint if triggered by user interaction), while useLayoutEffect runs synchronously after the commit phase but before the browser paints. useLayoutEffect is a recommended pattern for scenarios requiring the latest ref values before paint, such as measurements to avoid flicker. For keeping callback refs fresh specifically for external event listeners like EventSource (to avoid stale closures), the common pattern uses useRef updated in useLayoutEffect (the "latest ref pattern"). While not exclusively for EventSource, this ensures the ref holds the most current callback when setting up listeners in useEffect, preventing bugs from closures capturing outdated values. React 19 introduces useEffectEvent as a cleaner alternative for event callbacks inside effects that need latest values without deps.
Citations:
- 1: https://beta.reactjs.org/reference/react/useLayoutEffect
- 2: https://lqtiendev.com/difference-between-useeffect-and-uselayouteffect-updates-for-react-19-e8cc9c93e439
- 3: https://react.dev/reference/react/useLayoutEffect
- 4: https://kentcdodds.com/blog/useeffect-vs-uselayouteffect
- 5: https://react.dev/reference/react/useEffectEvent
- 6: https://www.epicreact.dev/the-latest-ref-pattern-in-react
🏁 Script executed:
head -200 components/frontend/src/hooks/use-agui-stream.ts | tail -160Repository: ambient-code/platform
Length of output: 5027
Use useLayoutEffect with a non-null ref to synchronously update the SSE callback, closing the race condition window.
The current useEffect at lines 94–97 runs asynchronously after paint. If an EventSource message arrives before that update completes, processEventRef.current?.(event) at line 137 will hit a stale or null handler, potentially causing buffering or lost events on initial mount.
Proposed fix
-import { useCallback, useEffect, useRef, useState } from 'react'
+import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
- const processEventRef = useRef<((event: PlatformEvent) => void) | null>(null)
+ const processEventRef = useRef<(event: PlatformEvent) => void>(() => {})
- useEffect(() => {
+ useLayoutEffect(() => {
processEventRef.current = processEvent
}, [processEvent])
- processEventRef.current?.(event)
+ processEventRef.current(event)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/frontend/src/hooks/use-agui-stream.ts` around lines 48 - 49,
Replace the async ref update with a synchronous one: initialize processEventRef
as a non-null no-op (useRef<(event: PlatformEvent) => void>(() => {})) and
change the effect that assigns processEventRef.current = processEvent from
useEffect to useLayoutEffect so the SSE handler
(processEventRef.current?.(event)) sees the updated callback immediately on
mount; keep the EventSource handler logic unchanged but ensure it calls
processEventRef.current(event).
…me contributors (#1039) ## Summary - **Group commits by author** with commit counts (e.g., "Gage Krumbach (18)"), sorted by contribution count - **Add "First-Time Contributors" section** with 🎉 emoji to celebrate new contributors - **Use Python** for reliable parsing of commit data with special characters - **Fix `--before` bug**: resolve tag to ISO date since `git log --before` requires a date, not a ref name — passing a tag name silently returns wrong results, causing incorrect first-timer detection ## Example Output (v0.0.34) ### Before (flat list): ``` - fix(frontend): resolve agent response buffering (#991) (de50276) - fix(frontend): export chat handles compacted MESSAGES_SNAPSHOT events (#1010) (b204abd) - fix(frontend): binary file download corruption (#996) (5b584f8) ... ``` ### After (grouped by author): ``` ## 🎉 First-Time Contributors - Derek Higgins - Pete Savage - Rahul Shetty ### Gage Krumbach (5) - fix(runner): improve ACP MCP tools (#1006) (26be0f9) - chore(manifests): scale up frontend replicas (#1008) (b331da1) ... ### Pete Savage (1) - fix(frontend): resolve agent response buffering (#991) (de50276) ``` ## Bug Fix: `--before` with tag names `git log --before=v0.0.33` does **not** filter by the tag's date — it resolves differently and returns commits from after the tag. This caused first-timer detection to produce wrong results. Fixed by resolving the tag to its ISO date first: ```python tag_date = subprocess.run( ['git', 'log', '-1', '--format=%ci', latest_tag], capture_output=True, text=True ).stdout.strip() ``` ## Test Plan - [x] Verified `--before=<tag>` vs `--before=<date>` returns different results - [x] Tested changelog generation locally against v0.0.33→v0.0.34 and v0.0.34→v0.0.35 - [x] Confirmed first-time contributor detection works correctly with date-based filtering - [x] YAML validates (`check-yaml` hook passes) - [ ] Will validate in next production release 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
ambient-code#991) …rce closure Fixes intermittent message buffering where agent responses don't display until the user sends another message. The EventSource onmessage handler was capturing a stale processEvent closure, causing events to be processed with outdated setState callbacks when component callbacks changed (e.g., during agent team operations). Use a ref to ensure the handler always calls the latest processEvent, guaranteeing immediate UI updates. --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
…me contributors (ambient-code#1039) ## Summary - **Group commits by author** with commit counts (e.g., "Gage Krumbach (18)"), sorted by contribution count - **Add "First-Time Contributors" section** with 🎉 emoji to celebrate new contributors - **Use Python** for reliable parsing of commit data with special characters - **Fix `--before` bug**: resolve tag to ISO date since `git log --before` requires a date, not a ref name — passing a tag name silently returns wrong results, causing incorrect first-timer detection ## Example Output (v0.0.34) ### Before (flat list): ``` - fix(frontend): resolve agent response buffering (ambient-code#991) (de50276) - fix(frontend): export chat handles compacted MESSAGES_SNAPSHOT events (ambient-code#1010) (b204abd) - fix(frontend): binary file download corruption (ambient-code#996) (5b584f8) ... ``` ### After (grouped by author): ``` ## 🎉 First-Time Contributors - Derek Higgins - Pete Savage - Rahul Shetty ### Gage Krumbach (5) - fix(runner): improve ACP MCP tools (ambient-code#1006) (26be0f9) - chore(manifests): scale up frontend replicas (ambient-code#1008) (b331da1) ... ### Pete Savage (1) - fix(frontend): resolve agent response buffering (ambient-code#991) (de50276) ``` ## Bug Fix: `--before` with tag names `git log --before=v0.0.33` does **not** filter by the tag's date — it resolves differently and returns commits from after the tag. This caused first-timer detection to produce wrong results. Fixed by resolving the tag to its ISO date first: ```python tag_date = subprocess.run( ['git', 'log', '-1', '--format=%ci', latest_tag], capture_output=True, text=True ).stdout.strip() ``` ## Test Plan - [x] Verified `--before=<tag>` vs `--before=<date>` returns different results - [x] Tested changelog generation locally against v0.0.33→v0.0.34 and v0.0.34→v0.0.35 - [x] Confirmed first-time contributor detection works correctly with date-based filtering - [x] YAML validates (`check-yaml` hook passes) - [ ] Will validate in next production release 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
…rce closure
Fixes intermittent message buffering where agent responses don't display until the user sends another message. The EventSource onmessage handler was capturing a stale processEvent closure, causing events to be processed with outdated setState callbacks when component callbacks changed (e.g., during agent team operations). Use a ref to ensure the handler always calls the latest processEvent, guaranteeing immediate UI updates.