Skip to content

feat(chat): route all chats through workflow; drop legacy /api/chat branch#1755

Closed
sweetmantech wants to merge 1 commit into
testfrom
feat/cutover-existing-chats-to-workflow
Closed

feat(chat): route all chats through workflow; drop legacy /api/chat branch#1755
sweetmantech wants to merge 1 commit into
testfrom
feat/cutover-existing-chats-to-workflow

Conversation

@sweetmantech
Copy link
Copy Markdown
Collaborator

@sweetmantech sweetmantech commented May 29, 2026

Summary

Completes the chat.recoupable.com cutover (#1747) by removing the legacy-vs-new conditional now that the Phase 2 backfill is in place. Every chat — new, existing-from-history, and the home landing — routes through recoup-api's /api/chat/workflow and reads history from chat_messages.

Three coordinated changes:

  • (a) Resolve sessionId for existing chats. /chat/[roomId] now renders ExistingChatBootstrap (useExistingChatBootstrap), which fetches GET /api/chats/[id] to recover the chat's sessionId before mounting <Chat>. Backfilled rooms carry a session_id (chats.id == rooms.id); a chat with no session / no access renders a graceful "isn't available" state.
  • (b) Read history from chat_messages. getChatMessages now calls GET /api/sessions/{sessionId}/chats/{chatId} (returns messages from selectChatMessages) instead of the memories-backed GET /api/chats/[id]/messages. useMessageLoader threads sessionId through.
  • (c) Delete the legacy /api/chat branch. Removed the if (!sessionId) fork in useChatTransport; sessionId is now required through useVercelChatVercelChatProviderChat (compiler-enforced — no sessionless mount). Migrated the home route / to bootstrap a session like /chat (NewChatBootstrap), and moved useAutoLogin into NewChatBootstrap so anonymous landings still prompt sign-in.

Dependencies (api, against test)

Merge gating

Do not merge to main ahead of the cutover bundle. Production still writes rooms/memories (cutover is on test), so rooms created after the backfill have no session/chat rows yet. This lands on main only with the rest of the bundle, after the final idempotent backfill re-run.

Behavior notes / risks

  • Eager provisioning: / now creates a session + sandbox on load (post-login landing, logo, SideMenu "New Chat" all hit /) — matches /chat today; accepted tradeoff.
  • Reopening any historical chat now routes through the workflow path, inheriting the documented accepted regressions (no MCP/Composio tools, artist context, title gen, send_email, Telegram) and inherited gaps (Stop, multi-tool traces, sandbox persistence).

Test plan

  • Unit: getChat (200/404/403/500) and getChatMessages (success / empty / not-ok / throw). 8 tests green.
  • tsc --noEmit clean on changed files; eslint clean.
  • Preview (needs api#625 reachable): open a backfilled /chat/[roomId] → history renders from chat_messages, follow-up message streams via workflow.
  • Preview: new chat from / and /chat → session provisioned, streams, URL → /chat/<id>.
  • Preview: anonymous visit to / → sign-in prompt (not a hung spinner).
  • Preview: unknown / deleted-account room id → graceful "isn't available".

🤖 Generated with Claude Code


Summary by cubic

Routes all chats (new and historical) through the workflow path and removes the legacy /api/chat branch. History now loads from chat_messages, completing the cutover.

  • New Features

    • /chat/[roomId] resolves sessionId via GET /api/chats/{id} using ExistingChatBootstrap; missing/no-access chats show “isn’t available”.
    • History reads from chat_messages via GET /api/sessions/{sessionId}/chats/{chatId}; useMessageLoader threads sessionId.
    • Legacy path removed; sessionId is required through ChatVercelChatProvideruseVercelChat → transport. Home / now bootstraps a session via NewChatBootstrap; useAutoLogin moved there to prompt sign-in for anonymous users.
  • Migration

    • Ship only with the cutover bundle after the final backfill; prod still writes rooms/memories. Requires recoupable/api#625 (runtime) and recoupable/api#627.

Written for commit 694fc72. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added support for loading and resuming existing chat sessions with improved authentication handling.
  • Refactor

    • Improved chat initialization flow with better session-based handling for both new and existing conversations.
    • Enhanced authentication bootstrap process to provide more reliable chat loading and resolution.

Review Change Stack

…ranch

Post Phase 2 backfill, existing chats can use the workflow path too, so
the legacy /api/chat conditional is no longer needed.

- (a) /chat/[roomId] resolves its sessionId via GET /api/chats/[id]
  (ExistingChatBootstrap / useExistingChatBootstrap) before mounting <Chat>;
  not-found/no-session rooms render a graceful state.
- (b) History loads from chat_messages via GET /api/sessions/{sessionId}/chats/{chatId}
  instead of the memories-backed /api/chats/[id]/messages.
- (c) Removed the if(!sessionId) legacy branch in useChatTransport; sessionId
  is now required through useVercelChat / VercelChatProvider / Chat. Migrated
  the home route (/) to bootstrap a session like /chat (NewChatBootstrap), and
  moved useAutoLogin into NewChatBootstrap so anonymous landings still prompt
  sign-in instead of hanging on the spinner.

Merges to main only inside the held cutover bundle, after the final backfill
re-run (prod still writes rooms/memories until the cutover lands on main).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview May 29, 2026 5:04pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR migrates the chat application from client-generated UUIDs and a legacy API endpoint to a workflow-based session system. It introduces bootstrap components for both new and existing chats, requires sessionId throughout the component and transport stack, and updates data fetching to use session-scoped endpoints. The legacy /api/chat path is removed from the transport layer in favor of /api/chat/workflow.

Changes

Workflow-based chat session migration

Layer / File(s) Summary
Chat data API contracts and fetching
lib/chats/getChat.ts, lib/messages/getChatMessages.ts
New getChat helper fetches existing chat metadata including sessionId. getChatMessages refactored to accept sessionId and call /api/sessions/{sessionId}/chats/{chatId} instead of the previous /api/chats/{chatId}/messages, returning UIMessage[] directly from the response.
Existing chat bootstrap hook and component
hooks/useExistingChatBootstrap.ts, components/VercelChat/ExistingChatBootstrap.tsx
New useExistingChatBootstrap hook uses Privy auth to resolve a chatId to { sessionId, chatId } state with loading/ready/not_found/error status transitions. ExistingChatBootstrap component conditionally renders Chat when ready, displays error or not-found messages on failure, or ChatSkeleton during loading.
Required sessionId through Chat/useVercelChat/useChatTransport/VercelChatProvider
components/VercelChat/chat.tsx, hooks/useVercelChat.ts, hooks/useChatTransport.ts, providers/VercelChatProvider.tsx
sessionId changed from optional to required across ChatProps, UseVercelChatProps, UseChatTransportOptions, and VercelChatProviderProps. useChatTransport removes the legacy /api/chat fallback path and always targets /api/chat/workflow with Privy-based auth token injection.
Message loader refactored for sessionId
hooks/useMessageLoader.ts
useMessageLoader signature updated to accept sessionId as first parameter; both sessionId and roomId must be present to trigger loading. getChatMessages now called with sessionId alongside roomId.
New chat bootstrap with auto-login
components/VercelChat/NewChatBootstrap.tsx
NewChatBootstrap calls useAutoLogin() before useNewChatBootstrap() to handle authentication for logged-out visitors and prevent loader hangs.
Page components switch to bootstrap components
app/page.tsx, components/Home/HomePage.tsx, app/chat/[roomId]/page.tsx
HomePage removes id prop and renders NewChatBootstrap with initialMessages. Chat room page imports and renders ExistingChatBootstrap with roomId. app/page.tsx removes generateUUID and no longer passes id to HomePage.

Sequence Diagram

sequenceDiagram
  participant User
  participant HomePage
  participant NewBoot as NewChatBootstrap
  participant useAutoLogin
  participant useNewBoot as useNewChatBootstrap
  participant Chat
  participant Privy
  participant WorkflowAPI as /api/chat/workflow
  
  User->>HomePage: Visit home /
  HomePage->>NewBoot: render(initialMessages)
  NewBoot->>useAutoLogin: call useAutoLogin()
  useAutoLogin->>Privy: ensure authentication
  Privy-->>useAutoLogin: authenticated or redirected
  NewBoot->>useNewBoot: call useNewChatBootstrap()
  useNewBoot->>Privy: getAccessToken()
  Privy-->>useNewBoot: accessToken
  useNewBoot-->>NewBoot: { sessionId, chatId }
  NewBoot->>Chat: render with sessionId + chatId
  Chat->>WorkflowAPI: POST /api/chat/workflow
  WorkflowAPI-->>Chat: response
Loading
sequenceDiagram
  participant User
  participant ChatRoom as /chat/[roomId]
  participant ExistBoot as ExistingChatBootstrap
  participant useExistBoot as useExistingChatBootstrap
  participant getChat
  participant Privy
  participant ChatAPI as /api/chats/{roomId}
  participant Chat
  participant WorkflowAPI as /api/chat/workflow
  
  User->>ChatRoom: Visit /chat/roomId
  ChatRoom->>ExistBoot: render(roomId)
  ExistBoot->>useExistBoot: call useExistingChatBootstrap(roomId)
  useExistBoot->>Privy: ensure authenticated
  useExistBoot->>Privy: getAccessToken()
  Privy-->>useExistBoot: accessToken
  useExistBoot->>getChat: fetch getChat(roomId, token)
  getChat->>ChatAPI: GET /api/chats/{roomId}
  ChatAPI-->>getChat: { sessionId, chatId, ... }
  getChat-->>useExistBoot: ApiChat
  useExistBoot-->>ExistBoot: { status: ready, sessionId, chatId }
  ExistBoot->>Chat: render with sessionId + chatId
  Chat->>WorkflowAPI: POST /api/chat/workflow
  WorkflowAPI-->>Chat: response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • recoupable/chat#1747: This PR implements the client-side cutover to the workflow endpoint—making sessionId required throughout the component stack, removing the legacy /api/chat transport path, adding bootstrap components to resolve sessionId, and updating message fetching to use the session-scoped API.

Possibly related PRs

  • recoupable/chat#1748: Both PRs refactor the chat bootstrap/transport flow to require sessionId and route through /api/chat/workflow, threading sessionId into Chat/useVercelChat/useChatTransport.
  • recoupable/chat#1618: Both PRs modify useMessageLoader and getChatMessages to thread sessionId through the message loading pipeline.

Suggested reviewers

  • cubic-dev-ai

Poem

🚀 A workflow replaces the UUID night,
Sessions now bootstrap the chat's flight,
Old /api/chat fades to the past,
Auth gates the flow so it's built to last,
One sessionId to rule them all—
New chats and old ones, they heed the call! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning PR violates DRY with 50+ Authorization header patterns repeated; unaddressed review about startedRef reset; inconsistent error UI styling; near-duplicate bootstrap state patterns. Extract authorization header utility; address review comment by making error terminal; unify error UI styling; consider abstracting bootstrap state patterns to reusable component.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cutover-existing-chats-to-workflow

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 694fc72324

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const startedRef = useRef(false);

useEffect(() => {
if (!authenticated) return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Trigger login before waiting for existing-chat auth

When a logged-out user opens /chat/[roomId] directly, this effect returns before fetching and ExistingChatBootstrap keeps rendering the skeleton; because <ChatContent> is only mounted after this hook reaches ready, its useAutoLogin() call never runs. This regresses the previous /chat/[roomId] behavior where <Chat> mounted immediately and prompted sign-in, so direct existing-chat links can hang indefinitely for unauthenticated non-mini-app users.

Useful? React with 👍 / 👎.

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

🧹 Nitpick comments (1)
components/VercelChat/ExistingChatBootstrap.tsx (1)

24-38: ⚡ Quick win

Announce the not_found/error states to assistive tech.

Both branches render their message as a plain <div>, so screen-reader users navigating into a freshly-resolved chat get no indication that it's unavailable or errored. Adding a role (and an aria-live for the async error) makes these status messages perceivable.

♿ Add status roles
   if (state.status === "not_found") {
     return (
       <div className="flex items-center justify-center h-dvh">
-        <div className="text-grey-dark-1">This chat isn’t available.</div>
+        <div role="status" className="text-grey-dark-1">This chat isn’t available.</div>
       </div>
     );
   }

   if (state.status === "error") {
     return (
       <div className="flex items-center justify-center h-dvh">
-        <div className="text-red-500 dark:text-red-400">{state.message}</div>
+        <div role="alert" className="text-red-500 dark:text-red-400">{state.message}</div>
       </div>
     );
   }

As per coding guidelines: "Provide proper ARIA roles/states and test with screen readers."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/VercelChat/ExistingChatBootstrap.tsx` around lines 24 - 38, The
"not_found" and "error" branches in ExistingChatBootstrap render plain <div>s so
screen-readers don't announce them; update the JSX in the ExistingChatBootstrap
component to add appropriate ARIA roles and live regions—e.g., mark the
not_found container as a status (role="status" and/or aria-live="polite") and
mark the error message container as an assertive live region (role="alert"
and/or aria-live="assertive") so state.status and state.message are exposed to
assistive tech; ensure the element containing {state.message} uses the
live/alert attributes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hooks/useExistingChatBootstrap.ts`:
- Around line 36-56: The catch block in the useExistingChatBootstrap hook
currently resets startedRef.current = false which allows implicit retries when
deps like getAccessToken change; instead, make the error terminal by removing
the reset of startedRef.current in the catch so startedRef continues to guard
against re-invocation (leave setState({ status: "error", message }) intact),
keeping retry behavior tied to route remounts (key={roomId}) as documented.

---

Nitpick comments:
In `@components/VercelChat/ExistingChatBootstrap.tsx`:
- Around line 24-38: The "not_found" and "error" branches in
ExistingChatBootstrap render plain <div>s so screen-readers don't announce them;
update the JSX in the ExistingChatBootstrap component to add appropriate ARIA
roles and live regions—e.g., mark the not_found container as a status
(role="status" and/or aria-live="polite") and mark the error message container
as an assertive live region (role="alert" and/or aria-live="assertive") so
state.status and state.message are exposed to assistive tech; ensure the element
containing {state.message} uses the live/alert attributes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 82f473c0-12d8-4e9a-886c-a00ef31d2820

📥 Commits

Reviewing files that changed from the base of the PR and between dd2afd4 and 694fc72.

⛔ Files ignored due to path filters (2)
  • lib/chats/__tests__/getChat.test.ts is excluded by !**/*.test.* and included by lib/**
  • lib/messages/__tests__/getChatMessages.test.ts is excluded by !**/*.test.* and included by lib/**
📒 Files selected for processing (13)
  • app/chat/[roomId]/page.tsx
  • app/page.tsx
  • components/Home/HomePage.tsx
  • components/VercelChat/ExistingChatBootstrap.tsx
  • components/VercelChat/NewChatBootstrap.tsx
  • components/VercelChat/chat.tsx
  • hooks/useChatTransport.ts
  • hooks/useExistingChatBootstrap.ts
  • hooks/useMessageLoader.ts
  • hooks/useVercelChat.ts
  • lib/chats/getChat.ts
  • lib/messages/getChatMessages.ts
  • providers/VercelChatProvider.tsx

Comment on lines +36 to +56
useEffect(() => {
if (!authenticated) return;
if (startedRef.current) return;
startedRef.current = true;

void (async () => {
try {
const accessToken = await getAccessToken();
const chat = await getChat(chatId, accessToken);
if (!chat || !chat.sessionId) {
setState({ status: "not_found" });
return;
}
setState({ status: "ready", sessionId: chat.sessionId, chatId: chat.id });
} catch (error) {
startedRef.current = false;
const message = error instanceof Error ? error.message : "Failed to load chat.";
setState({ status: "error", message });
}
})();
}, [authenticated, chatId, getAccessToken]);
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 | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Is the getAccessToken function returned by usePrivy() in @privy-io/react-auth referentially stable / memoized across renders?

💡 Result:

Answer: No definitive public guarantee was found that getAccessToken returned by usePrivy() is referentially stable/memoized across renders. What Privy docs confirm: - Privy documents that you can retrieve the token via const { getAccessToken } = usePrivy(); and then call await getAccessToken(); it also notes that getAccessToken “will also automatically refresh” the token if it’s near expiry/expired. [1] What Privy docs do not confirm: - The docs do not state that getAccessToken’s function identity is memoized (stable) across component re-renders, nor do they mention useCallback/memoization behavior for that specific method. [1] Evidence from official package pages: - The publicly visible package/registry/README content similarly shows the usePrivy() API surface but does not document referential-stability guarantees for getAccessToken. [2] Practical implication: - Because no source explicitly guarantees stability, you should assume the function reference could change when usePrivy() re-runs due to context/state updates, unless you verify empirically in your app (e.g., log the reference across renders) or inspect the exact installed package source for your version. If you want, tell me your installed @privy-io/react-auth version (e.g., 3.24.0-beta…) and whether you’re using React strict mode; then I can try to locate and cite the exact implementation for that version to answer definitively.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show privy version(s)
ls -la
rg -n "\"`@privy-io/react-auth`\"" package.json package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null || true

# Inspect the hook file around the cited lines and any surrounding logic (startedRef, roomId key, retries)
HOOK_FILE="hooks/useExistingChatBootstrap.ts"
if [ -f "$HOOK_FILE" ]; then
  wc -l "$HOOK_FILE"
  sed -n '1,120p' "$HOOK_FILE" | nl -ba
else
  echo "Missing $HOOK_FILE"
fi

Repository: recoupable/chat

Length of output: 2236


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the hook file fully with line numbers
HOOK_FILE="hooks/useExistingChatBootstrap.ts"
if [ -f "$HOOK_FILE" ]; then
  echo "=== $HOOK_FILE ==="
  cat -n "$HOOK_FILE"
else
  echo "Missing $HOOK_FILE"
fi

# Search for React keys involving roomId (e.g., key={roomId})
rg -n --hidden --no-ignore-vcs "key=\{[^}]*roomId[^}]*\}" . || true
rg -n --hidden --no-ignore-vcs "roomId[^\n]{0,80}key=" . || true

Repository: recoupable/chat

Length of output: 3010


Align the error behavior with the hook’s “StrictMode-only” startedRef guard

useExistingChatBootstrap documents that startedRef only guards StrictMode’s double-invoke and that re-resolution happens via key={roomId} route remounts. However, the catch block resets startedRef.current = false, which implicitly allows additional fetch attempts later if the useEffect deps change (notably getAccessToken—Privy does not document referential stability for that function). Since remounting on route change already covers “retry”, keeping the error state terminal avoids this unintended coupling.

♻️ Make the error state terminal (drop the implicit retry)
       } catch (error) {
-        startedRef.current = false;
         const message = error instanceof Error ? error.message : "Failed to load chat.";
         setState({ status: "error", message });
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!authenticated) return;
if (startedRef.current) return;
startedRef.current = true;
void (async () => {
try {
const accessToken = await getAccessToken();
const chat = await getChat(chatId, accessToken);
if (!chat || !chat.sessionId) {
setState({ status: "not_found" });
return;
}
setState({ status: "ready", sessionId: chat.sessionId, chatId: chat.id });
} catch (error) {
startedRef.current = false;
const message = error instanceof Error ? error.message : "Failed to load chat.";
setState({ status: "error", message });
}
})();
}, [authenticated, chatId, getAccessToken]);
useEffect(() => {
if (!authenticated) return;
if (startedRef.current) return;
startedRef.current = true;
void (async () => {
try {
const accessToken = await getAccessToken();
const chat = await getChat(chatId, accessToken);
if (!chat || !chat.sessionId) {
setState({ status: "not_found" });
return;
}
setState({ status: "ready", sessionId: chat.sessionId, chatId: chat.id });
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to load chat.";
setState({ status: "error", message });
}
})();
}, [authenticated, chatId, getAccessToken]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/useExistingChatBootstrap.ts` around lines 36 - 56, The catch block in
the useExistingChatBootstrap hook currently resets startedRef.current = false
which allows implicit retries when deps like getAccessToken change; instead,
make the error terminal by removing the reset of startedRef.current in the catch
so startedRef continues to guard against re-invocation (leave setState({ status:
"error", message }) intact), keeping retry behavior tied to route remounts
(key={roomId}) as documented.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 15 files

Confidence score: 3/5

  • There is a concrete user-facing regression risk: unauthenticated visitors can be left in a permanent loading state due to early-return/bootstrap flow changes in hooks/useExistingChatBootstrap.ts and components/VercelChat/ExistingChatBootstrap.tsx.
  • The auth/bootstrap sequencing appears fragile right now; when auth prompting no longer runs before bootstrap resolution on /chat/[roomId], users may not reach sign-in or a terminal state.
  • There is additional medium risk from repeated bootstrap attempts if getAccessToken identity changes and from duplicate useAutoLogin hook instances triggering overlapping login() calls in components/VercelChat/NewChatBootstrap.tsx.
  • Pay close attention to hooks/useExistingChatBootstrap.ts, components/VercelChat/ExistingChatBootstrap.tsx, components/VercelChat/NewChatBootstrap.tsx - prevent stuck loading and duplicate auth/bootstrap retries.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="hooks/useExistingChatBootstrap.ts">

<violation number="1" location="hooks/useExistingChatBootstrap.ts:37">
P2: This early return leaves unauthenticated visitors stuck in the loading state instead of progressing to sign-in or a terminal state.</violation>

<violation number="2" location="hooks/useExistingChatBootstrap.ts:51">
P2: The `startedRef.current = false` reset in the `catch` block allows unintended re-fetches if `getAccessToken` (which is in the dependency array and not guaranteed to be referentially stable by Privy) changes identity. Since `key={roomId}` already handles remounting for route changes, the error state should be terminal — remove this reset to prevent the hook from silently retrying on unrelated re-renders.</violation>
</file>

<file name="components/VercelChat/NewChatBootstrap.tsx">

<violation number="1" location="components/VercelChat/NewChatBootstrap.tsx:27">
P2: `useAutoLogin` is now called in both bootstrap and chat content, which can trigger duplicate `login()` attempts from separate hook instances.</violation>
</file>

<file name="components/VercelChat/ExistingChatBootstrap.tsx">

<violation number="1" location="components/VercelChat/ExistingChatBootstrap.tsx:40">
P2: Anonymous visits to `/chat/[roomId]` can get stuck on a permanent loading skeleton because auth prompting no longer runs before bootstrap resolution.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}
setState({ status: "ready", sessionId: chat.sessionId, chatId: chat.id });
} catch (error) {
startedRef.current = false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The startedRef.current = false reset in the catch block allows unintended re-fetches if getAccessToken (which is in the dependency array and not guaranteed to be referentially stable by Privy) changes identity. Since key={roomId} already handles remounting for route changes, the error state should be terminal — remove this reset to prevent the hook from silently retrying on unrelated re-renders.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useExistingChatBootstrap.ts, line 51:

<comment>The `startedRef.current = false` reset in the `catch` block allows unintended re-fetches if `getAccessToken` (which is in the dependency array and not guaranteed to be referentially stable by Privy) changes identity. Since `key={roomId}` already handles remounting for route changes, the error state should be terminal — remove this reset to prevent the hook from silently retrying on unrelated re-renders.</comment>

<file context>
@@ -0,0 +1,59 @@
+        }
+        setState({ status: "ready", sessionId: chat.sessionId, chatId: chat.id });
+      } catch (error) {
+        startedRef.current = false;
+        const message = error instanceof Error ? error.message : "Failed to load chat.";
+        setState({ status: "error", message });
</file context>

const startedRef = useRef(false);

useEffect(() => {
if (!authenticated) return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This early return leaves unauthenticated visitors stuck in the loading state instead of progressing to sign-in or a terminal state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useExistingChatBootstrap.ts, line 37:

<comment>This early return leaves unauthenticated visitors stuck in the loading state instead of progressing to sign-in or a terminal state.</comment>

<file context>
@@ -0,0 +1,59 @@
+  const startedRef = useRef(false);
+
+  useEffect(() => {
+    if (!authenticated) return;
+    if (startedRef.current) return;
+    startedRef.current = true;
</file context>

export default function NewChatBootstrap({
initialMessages,
}: NewChatBootstrapProps) {
useAutoLogin();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: useAutoLogin is now called in both bootstrap and chat content, which can trigger duplicate login() attempts from separate hook instances.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/VercelChat/NewChatBootstrap.tsx, line 27:

<comment>`useAutoLogin` is now called in both bootstrap and chat content, which can trigger duplicate `login()` attempts from separate hook instances.</comment>

<file context>
@@ -3,22 +3,28 @@
 export default function NewChatBootstrap({
   initialMessages,
 }: NewChatBootstrapProps) {
+  useAutoLogin();
   const state = useNewChatBootstrap();
 
</file context>

);
}

return <ChatSkeleton />;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Anonymous visits to /chat/[roomId] can get stuck on a permanent loading skeleton because auth prompting no longer runs before bootstrap resolution.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/VercelChat/ExistingChatBootstrap.tsx, line 40:

<comment>Anonymous visits to `/chat/[roomId]` can get stuck on a permanent loading skeleton because auth prompting no longer runs before bootstrap resolution.</comment>

<file context>
@@ -0,0 +1,41 @@
+    );
+  }
+
+  return <ChatSkeleton />;
+}
</file context>

@sweetmantech
Copy link
Copy Markdown
Collaborator Author

Closing — superseded by the incremental cutover shipped via chat#1747 on 2026-06-01.

This was the Approach A monolithic cutover that bundled (a) ExistingChatBootstrap for /chat/[roomId], (b) reading history from chat_messages via the new canonical reader, and (c) deletion of the legacy /api/chat branch. The team pivoted to shipping each piece as its own focused PR: chat#1752 shipped (a) + (b); chat#1748 + chat#1760 routed every chat mount through the workflow path; and chat#1765 (open) finishes (c) — making sessionId compile-time required and deleting app/chat/[roomId]/page.tsx.

Patterns from this PR (the useChatTransport fork removal, the type tightening through useVercelChat / VercelChatProvider / <Chat>) are reused verbatim in chat#1765.

@sweetmantech sweetmantech deleted the feat/cutover-existing-chats-to-workflow branch June 1, 2026 20:01
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.

1 participant