Skip to content

fix(frontend): resolve agent response buffering due to stale EventSou…#991

Merged
Gkrumbach07 merged 2 commits intoambient-code:mainfrom
psav:psav/fix-message-buffering
Mar 24, 2026
Merged

fix(frontend): resolve agent response buffering due to stale EventSou…#991
Gkrumbach07 merged 2 commits intoambient-code:mainfrom
psav:psav/fix-message-buffering

Conversation

@psav
Copy link
Copy Markdown
Contributor

@psav psav commented Mar 23, 2026

…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.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 23, 2026

Walkthrough

Replaced direct processEvent usage in the SSE handler with a ref-backed callback in use-agui-stream to avoid stale-closure behavior; added processEventRef and a syncing useEffect, and removed processEvent from the connect hook dependency list.

Changes

Cohort / File(s) Summary
EventSource stale-closure fix
components/frontend/src/hooks/use-agui-stream.ts
Added processEventRef ref and a useEffect to keep it synchronized with processEvent. Updated eventSource.onmessage to call processEventRef.current?.(event) and removed processEvent from the connect hook dependency list.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main fix: resolving a stale EventSource closure causing message buffering in the frontend.
Description check ✅ Passed The description is directly related to the changeset, explaining the stale closure bug and how the ref-based solution addresses it.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Remove processEvent from connect's dependency array.

Since the handler now accesses processEvent through the ref rather than capturing it directly, processEvent should be removed from the dependency array at line 188. Keeping it causes unnecessary recreations of connect whenever processEvent changes, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6b21778 and 60ca537.

📒 Files selected for processing (1)
  • components/frontend/src/hooks/use-agui-stream.ts

@ambient-code ambient-code bot added this to the Review Queue milestone Mar 24, 2026
@Gkrumbach07
Copy link
Copy Markdown
Contributor

@psav can u look at the code rabbit comment. is it accurate

Remove processEvent from connect's dependency array.

Since the handler now accesses processEvent through the ref rather than capturing it directly, processEvent should be removed from the dependency array at line 188. Keeping it causes unnecessary recreations of connect whenever processEvent changes, which defeats part of the purpose of using the ref pattern.****

@jeremyeder
Copy link
Copy Markdown
Contributor

I rechecked the current head, and these points are already addressed:

  • processEventRef is synchronized with useLayoutEffect
  • processEvent has been removed from connect’s dependency array
  • there is a regression test covering rerender without reconnecting and asserting the latest callback is used

So my earlier concern was accurate for an earlier revision, but it’s stale against the current PR state.

psav and others added 2 commits March 24, 2026 16:48
…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>
@jeremyeder jeremyeder force-pushed the psav/fix-message-buffering branch from 27e55a7 to e1f5bbc Compare March 24, 2026 21:03
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 27e55a7 and e1f5bbc.

📒 Files selected for processing (1)
  • components/frontend/src/hooks/use-agui-stream.ts

Comment on lines +48 to +49
// Store latest processEvent to avoid stale closure in EventSource handler
const processEventRef = useRef<((event: PlatformEvent) => void) | null>(null)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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


🏁 Script executed:

head -200 components/frontend/src/hooks/use-agui-stream.ts | tail -160

Repository: 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).

@Gkrumbach07 Gkrumbach07 merged commit de50276 into ambient-code:main Mar 24, 2026
27 of 28 checks passed
jeremyeder added a commit that referenced this pull request Mar 25, 2026
…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 ambient-code bot removed this from the Review Queue milestone Mar 26, 2026
jeremyeder pushed a commit to jeremyeder/platform that referenced this pull request Mar 26, 2026
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>
jeremyeder added a commit to jeremyeder/platform that referenced this pull request Mar 26, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants