feat(example): GitHub OAuth for the assistant + resume-stream stability fixes#1374
Merged
Conversation
Copy the GitHub auth layer from examples/auth-agent into examples/assistant so each user gets their own MyAssistant Durable Object scoped to their GitHub login. The Worker handles /auth/* and forwards /chat* to the user-scoped agent via getAgentByName. The client gates the existing Think UI behind a GitHub sign-in and adds a sign-out button to the header. Made-with: Cursor
🦋 Changeset detectedLatest commit: 7294a92 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
agents
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
Previously, Think unconditionally sent `cf_agent_chat_messages` from `onConnect` on every WebSocket connection. When a client refreshed mid-stream, that broadcast arrived in the same connect sequence as `cf_agent_stream_resuming` and overwrote the in-progress assistant message the client was about to rebuild from the resumed stream. The assistant reply would stay hidden until the server finished the turn and re-broadcast the persisted history. Only broadcast `CHAT_MESSAGES` on connect when there is no active resumable stream. During an active stream the resume flow is the authoritative source of state — STREAM_RESUMING triggers chunk replay, and the final state broadcast happens when the turn completes. This matches the behavior that AIChatAgent already had. Promote the internal `_resumableStream` field to `protected` so framework subclasses and focused tests can coordinate around the resume lifecycle. Regression test in `onconnect-broadcast.test.ts` covers all three states: no active stream (broadcasts), active stream (suppresses broadcast + sends STREAM_RESUMING), and post-completion (back to broadcasting). Made-with: Cursor
…ansitions
`useAgentChat` was recreating the AI SDK Chat instance and orphaning
any in-flight `resumeStream` whenever `agent.name` transitioned in
place. This broke stream resumption on page refresh for any consumer
using `useAgent({ basePath })` together with
`static options = { sendIdentityOnConnect: true }`: the client starts
with a placeholder name ("default"), then useAgent mutates
`agent.name` to the server-assigned value when the identity frame
arrives. The resulting change to `stableChatIdRef` flipped the `id`
prop passed to `useChat`, and AI SDK's `shouldRecreateChat` check then
replaced the Chat instance.
The useEffect that fires `chatRef.current.resumeStream()` is keyed on
the ref object, not the Chat instance, so it does not re-fire on
recreation. The orphaned Chat kept feeding replayed chunks into its
own state while React subscribed to the new Chat's state — so the
user saw an empty assistant reply after a mid-stream refresh until
the server's final `CF_AGENT_CHAT_MESSAGES` broadcast landed.
Distinguish an in-place `agent.name` mutation from a genuine
"consumer switched chats" event by checking the agent object's
reference identity:
- same `agent` reference, `.name` mutated → not a chat switch; keep
the Chat instance stable.
- new `agent` reference → chat switch; recompute the stable chat id
so the AI SDK recreates the Chat against the new conversation.
The one-time URL-arrival upgrade (fallback → resolved key when the
WebSocket handshake completes) still runs.
Two regression tests in `react-tests/use-agent-chat.test.tsx`:
in-place name mutation (Chat stable) and agent-object swap (Chat
recreates). The existing `should refetch initial messages when the
agent name changes` test also continues to pass — it already used
distinct agent object references.
Consumers who want to switch chats without remounting should pass a
different `agent` object (e.g. a new `useAgent({...})` call with a
different `name`). For a completely fresh Chat, the conventional
React pattern — `key={chatId}` on the parent or swapping the subtree
— continues to work.
Made-with: Cursor
Tightens the assistant example after real-world testing:
- wrangler.jsonc: drop `/agents/*` from `run_worker_first`. The
previous config routed all `/agents/*` requests to the Worker and
fell back to `routeAgentRequest`, which let any client reach
`/agents/my-assistant/<login>` unauthenticated and talk directly to
that user's Durable Object. Narrow the Worker routes to only the
OAuth endpoints and `/chat*`, and drop the `routeAgentRequest`
fallback in `src/server.ts`. The only way to reach MyAssistant is
now the GitHub-authenticated `/chat*` path, which resolves the DO
instance via `getAgentByName(env.MyAssistant, user.login)`.
- client.tsx: unify the two "loading" UIs. `useAgentChat` Suspense-
fires during the initial `/get-messages` HTTP fetch, which
previously flashed an ugly default "Loading..." string between the
auth-shell spinner and the ready chat. Wrap just the authenticated
`<Chat>` in a `Suspense` fallback that reuses `LoadingView` with a
contextual message so the shell stays consistent across the auth-
check and chat-ready phases.
- server.ts: opt MyAssistant into `sendIdentityOnConnect: true` so
the client knows which DO it ended up talking to (this is the
canonical companion to `useAgent({ basePath })`). This is safe now
that the ai-chat stableChatIdRef fix in the same PR prevents the
resulting `agent.name` transition from orphaning the Chat instance.
- wip/think-multi-session-assistant-plan.md: clear out an outdated
"known wart" paragraph that was written during debugging against
an incorrect hypothesis. Replace with a short historical note now
that the actual library bugs are fixed.
Made-with: Cursor
`addMcpServer(name, url)` with no explicit `callbackPath` falls back
to `${host}/agents/my-assistant/${this.name}/callback`. On this
example, `sendIdentityOnConnect: true` disables the framework's
normal enforcement that would have required `callbackPath`, so this
default slipped through silently.
With the previous commit narrowing `run_worker_first` to `/auth/*`
and `/chat*` (and dropping the `routeAgentRequest` fallback), those
`/agents/...` callback URLs are no longer routed to the Worker —
the SPA asset handler serves `index.html` with a 200, the DO never
sees the OAuth `code`, and the server hangs in `AUTHENTICATING`.
Pass `callbackPath: "chat/mcp-callback"` so the OAuth redirect
follows the same authenticated path as the rest of the app: Worker
authenticates the GitHub cookie, forwards to the user's MyAssistant
DO, and `Agent._onRequest` dispatches the callback via
`mcp.isCallbackRequest()` (which matches on the stored origin +
pathname).
Made-with: Cursor
Merged
1 task
threepointone
added a commit
that referenced
this pull request
Apr 24, 2026
- Mark "PR 2" as shipped in two parts: 2a (auth + stability, #1374) already landed, 2b (parent/child refactor + useChats prototype) is the next action. - Add a short history of what landed in #1374 (auth, security fix, Think onConnect fix, ai-chat stableChatIdRef fix, MCP callback routing). - Queue #1378 as a follow-up from PR 2a. - Refresh "Current status" and "Likely next action" accordingly. Made-with: Cursor
Merged
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR started as "add GitHub OAuth to the assistant example" and grew to include two library fixes that fell out of actually deploying it and exercising mid-stream refresh, plus a follow-on MCP-routing fix caught in review. The branch is structured as focused commits so reviewers can look at each concern independently; the two
fix(...)library commits are self-contained and can be split into their own PRs if preferred.feat(example): add GitHub OAuth to the assistant(8223667)examples/auth-agentintoexamples/assistant, so the assistant is deployable and user-scoped without any browser-chosen DO names./auth/*and forwards/chat*to the authenticated user'sMyAssistantinstance viagetAgentByName(env.MyAssistant, user.login).useAgent({ agent: "MyAssistant", basePath: "chat" })..env.examplewalk through the OAuth setup and deployment steps.fix(example): close auth bypass + polish assistant UX(18e9202)/agents/*through the Worker and fell back torouteAgentRequest, which let anyone reach/agents/my-assistant/<login>unauthenticated and talk directly to that user's Durable Object. The Worker routes are now narrowed to/auth/*and/chat*, and therouteAgentRequestfallback is gone. The only path intoMyAssistantis now the GitHub-authenticated/chat*route.useAgentChatSuspense fallback) so the shell stays consistent across phases.MyAssistantintosendIdentityOnConnect: true, safe now that the ai-chat fix below prevents the resultingagent.nametransition from orphaning the Chat instance.fix(think): do not broadcast CHAT_MESSAGES while a stream is in flight(ff19bde)Think was unconditionally sending
cf_agent_chat_messageson every WebSocket connect. On a mid-stream refresh that broadcast arrived in the same connect sequence ascf_agent_stream_resumingand overwrote the in-progress assistant message the client was about to rebuild from the resumed stream — the reply stayed hidden until the turn completed. NowonConnectsuppresses that broadcast when_resumableStream.hasActiveStream()is true; resume is the authoritative source of state during an in-flight turn. Matches whatAIChatAgentalready did. Regression tests cover all three states (no active stream, active stream, post-completion).fix(ai-chat): keep Chat instance stable across agent.name identity transitions(8788939)Even after the Think fix above, mid-stream refresh still didn't resume.
useAgentChatwas recreating the AI SDK Chat instance wheneveragent.namemutated in place — which is exactly whatuseAgent({ basePath })+sendIdentityOnConnect: truedoes (placeholder "default" → server-assigned login). The recreated Chat orphaned the in-flightresumeStream(), and since the resume effect is keyed on the ref, it didn't re-fire.stableChatIdRefnow distinguishes an in-place name mutation from a genuine chat switch by checking theagentobject's reference identity; the one-time URL-arrival upgrade (fallback → resolved key) still runs. Two regression tests cover the in-place mutation and the agent-object swap.fix(example): route MCP OAuth callbacks through /chat*(7294a92)Caught in review on this PR.
addMcpServer(name, url)with no explicitcallbackPathfalls back to${host}/agents/my-assistant/${this.name}/callback. Previously that worked because/agents/*was routed to the Worker with arouteAgentRequestfallback. After narrowingrun_worker_firstto/auth/*and/chat*(the security fix above), that default callback URL hits the SPA asset handler, servesindex.htmlwith a 200, and the DO never sees the OAuthcode— MCP servers hang inAUTHENTICATING.sendIdentityOnConnect: trueincidentally disables the framework's enforcement that would normally requirecallbackPath, so it slipped through silently. PassingcallbackPath: "chat/mcp-callback"routes the redirect through the same authenticated/chat*dispatcher, andAgent._onRequestpicks up the callback viamcp.isCallbackRequest().Changesets
.changeset/think-onconnect-no-clobber-resume.md(@cloudflare/think, patch).changeset/ai-chat-stable-chat-id.md(@cloudflare/ai-chat, patch)Test plan
npm run cipackages/think/src/tests/onconnect-broadcast.test.ts, two new cases inpackages/ai-chat/src/react-tests/use-agent-chat.test.tsxnpm start, sign in, start a long-running "tell me a story" stream, refresh mid-stream — the stream now resumes and the in-progress assistant message is preserved.READY.Follow-ups
The multi-session
Chats/useChats()prototype is still a follow-up PR, perwip/think-multi-session-assistant-plan.md.Made with Cursor