Summary
When the name prop passed to useAgent changes (e.g. on logout → new guest session), useAgentChat's initial messages fetch fires during the same render using the old socket's URL — which still contains the previous session's auth token. This causes two problems:
- Credential leak: a signed-in user's auth token is sent to a new (guest) endpoint after logout.
- Data poisoning: the old conversation's messages are cached under the new session's cache key. Because the key is already populated when the new socket's URL arrives, no corrective re-fetch occurs — the stale messages persist for the lifetime of the component.
Root cause
In packages/agents/src/chat/react.tsx, initialMessagesCacheKey is prop-derived (agentAddressKey, set synchronously from options.name every render). When name changes:
agentAddressKey → new value (sync, this render)
requestCache miss → doGetInitialMessages fires immediately, with agentUrlString = agent.getHttpUrl() = the old socket's _url (still carries old conversationId in path and old ?token=...)
useStableSocket replaces the socket in a useEffect — after this render — so _url is still stale when the fetch fires
The fetch URL and the cache key are therefore from different renders, violating the invariant that was safe in @cloudflare/ai-chat@0.4.x (where the cache key was derived from agentUrl.pathname, which lagged with the socket).
Why the obvious fix (use resolvedInitialMessagesCacheKey) regresses #1356
resolvedInitialMessagesCacheKey (line 768) is the URL-inclusive key that would fix this, but it was deliberately excluded from doGetInitialMessages because including it causes a Suspense re-trigger on the null → resolved URL transition at first mount. The comment at lines 755–763 describes this exactly.
Reproduction
- Mount
useAgentChat with useAgent({ name: convA, query: { token: tokenA } })
- Change props to
{ name: convB, query: { token: tokenB } }
- Observe:
/get-messages fires with the URL for convA?token=tokenA
- The returned messages (from convA) are cached under convB's key
- When the new socket arrives with convB's URL, no re-fetch occurs (cache hit on the poisoned entry)
- Chat displays convA's messages under convB's session
What would fix it
The two cases that need different behavior:
| Transition |
Desired behavior |
agentUrl null → resolved (first mount handshake) |
Do NOT bust cache — existing behavior, avoids #1356 |
name changes while agentUrl is already non-null (session switch) |
DO use URL-inclusive key — wait for new socket URL before fetching |
A fix that distinguishes these two cases (e.g. tracking whether the previous agentUrl.pathname was non-null before the name change) would restore the 0.4.x invariant without reintroducing #1356.
Workaround
Key the component mounting useAgentChat on name (force remount on session switch). This avoids the stale-fetch render entirely since the new mount's agent.getHttpUrl() is already correct.
Versions
agents: 0.17.3 (latest)
@cloudflare/ai-chat: 0.9.3 (latest, now a re-export of agents/chat/react)
- Regression introduced when
useAgentChat moved from @cloudflare/ai-chat/dist/react.js (0.4.x, inline) into agents/src/chat/react.tsx
Summary
When the
nameprop passed touseAgentchanges (e.g. on logout → new guest session),useAgentChat's initial messages fetch fires during the same render using the old socket's URL — which still contains the previous session's auth token. This causes two problems:Root cause
In
packages/agents/src/chat/react.tsx,initialMessagesCacheKeyis prop-derived (agentAddressKey, set synchronously fromoptions.nameevery render). Whennamechanges:agentAddressKey→ new value (sync, this render)requestCachemiss →doGetInitialMessagesfires immediately, withagentUrlString=agent.getHttpUrl()= the old socket's_url(still carries old conversationId in path and old?token=...)useStableSocketreplaces the socket in auseEffect— after this render — so_urlis still stale when the fetch firesThe fetch URL and the cache key are therefore from different renders, violating the invariant that was safe in
@cloudflare/ai-chat@0.4.x(where the cache key was derived fromagentUrl.pathname, which lagged with the socket).Why the obvious fix (use
resolvedInitialMessagesCacheKey) regresses #1356resolvedInitialMessagesCacheKey(line 768) is the URL-inclusive key that would fix this, but it was deliberately excluded fromdoGetInitialMessagesbecause including it causes a Suspense re-trigger on the null → resolved URL transition at first mount. The comment at lines 755–763 describes this exactly.Reproduction
useAgentChatwithuseAgent({ name: convA, query: { token: tokenA } }){ name: convB, query: { token: tokenB } }/get-messagesfires with the URL forconvA?token=tokenAWhat would fix it
The two cases that need different behavior:
agentUrlnull → resolved (first mount handshake)namechanges whileagentUrlis already non-null (session switch)A fix that distinguishes these two cases (e.g. tracking whether the previous
agentUrl.pathnamewas non-null before thenamechange) would restore the 0.4.x invariant without reintroducing #1356.Workaround
Key the component mounting
useAgentChatonname(force remount on session switch). This avoids the stale-fetch render entirely since the new mount'sagent.getHttpUrl()is already correct.Versions
agents: 0.17.3 (latest)@cloudflare/ai-chat: 0.9.3 (latest, now a re-export ofagents/chat/react)useAgentChatmoved from@cloudflare/ai-chat/dist/react.js(0.4.x, inline) intoagents/src/chat/react.tsx