Promote chat-workflow cutover to main#1766
Conversation
* feat: route new chats through /api/chat/workflow New chats now provision a recoup-api session + sandbox before the first message and stream through recoup-api's /api/chat/workflow endpoint (Path C cutover, #1747 Phase 1 + 3). Existing chats opened from history continue using the legacy /api/chat until Phase 2 backfills session_id onto those rows. URL builders, kebab helper, sandbox provisioner, and error class are ported from open-agents (DRY) so chat.recoupable.com and sandbox.recoupable.com construct identical cloneUrls and hit the same recoup-api endpoints. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor: source cloneUrl from api response, drop client URL builders POST /api/sessions now derives cloneUrl server-side via ensurePersonalRepo and returns it on the session row (api#618, api#620). It also accepts organizationId so org access is validated centrally. Cut over to that contract: - createSession body shrunk to {title?, organizationId?}; cloneUrl/branch/ sandboxType were silently dropped by api's Zod schema - NewChatBootstrap calls createSession({organizationId: selectedOrgId}) and reads session.cloneUrl off the response before createSandbox - Delete buildOrgRepoUrl, buildPersonalRepoUrl, githubOwner, toKebabCase — client-side URL construction is now dead code, and buildPersonalRepoUrl produced the pre-backfill <slug>-<accountId> shape that no longer exists in GitHub (api#618) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: use api-minted chat.id as the surface id The server page was minting its own UUID via generateUUID() and the api was minting a different one inside createSessionHandler. The chat component used the server UUID for the workflow POST body, but api looked up the chat row by id and returned 404 "Chat not found". Drop the server-side UUID; capture chat.id off the createSession response and pass that to <Chat>. The URL navigation now lands at /chat/<api-chatId> so the row exists when the workflow looks it up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: split bootstrap hook + sandbox helpers per SRP review Per PR review: - Extract bootstrap state machine into hooks/useNewChatBootstrap; NewChatBootstrap is now a thin renderer. - Split createSandbox into focused files: parseCreateSandboxErrorResponse, getFallbackSandboxCreateErrorMessage, plus lib/string/getOptionalString for the generic non-empty-string helper. - Clarify SandboxCreateRequestError JSDoc — it wraps the chat→recoup-api HTTP boundary, not the @vercel/sandbox SDK (which lives server-side inside recoup-api). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
… render (#1751) chat was pinned to ai@6.0.0-beta.99 / @ai-sdk/react@3.0.0-beta.99. The beta `tool-output-available` UIMessage chunk is a z.strictObject without a `providerMetadata` field, so the workflow path (recoup-api emits that field from ai@6.0.x stable) triggered AI_TypeValidationError on the chat client — the UI stalled at "Using bash" with no tool output rendered (#1747). Align with open-agents' proven pair (ai@6.0.165 / @ai-sdk/react@3.0.167), which carries `providerMetadata` on the tool-output-available schema, and move the provider packages off beta to match: - ai 6.0.0-beta.99 -> 6.0.165 - @ai-sdk/react 3.0.0-beta.99 -> 3.0.167 - @ai-sdk/anthropic 3.0.0-beta.53 -> 3.0.80 - @ai-sdk/google 3.0.0-beta.44 -> 3.0.80 - @ai-sdk/openai 3.0.0-beta.59 -> 3.0.66 tsc --noEmit clean after aligning ai + @ai-sdk/react on a single version. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ader (#1752) * feat(chat): add canonical /sessions/[sessionId]/chats/[chatId] route Mounts <Chat id sessionId> for the canonical workflow URL. Loader plumbing (useMessageLoader → /api/sessions/{sid}/chats/{cid}) ships in E2; this PR is route-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chat): wire useMessageLoader to session-scoped endpoint useMessageLoader takes (sessionId, chatId) and fetches from GET /api/sessions/{sid}/chats/{cid}; legacy memories path deleted. Skips when either id is missing — chats opened without a sessionId (legacy /chat/{roomId} route) render an empty transcript instead of falling back to memories. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1757) After a first message send, the URL bar is rewritten to /sessions/{sessionId}/chats/{id} when sessionId is known. Surfaces without sessionId (the / homepage today) skip the rewrite — they get the canonical URL once the homepage is moved onto NewChatBootstrap in a later cleanup. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: sweetman.eth <sweetmantech@gmail.com>
…ChatBootstrap (#1760) * refactor(chat): remove UUID generation and update HomePage to use NewChatBootstrap This commit eliminates the server-side UUID generation in Home and updates the HomePage component to use NewChatBootstrap instead of Chat. The changes streamline the chat initialization process by relying on the NewChatBootstrap for session management and message provisioning. * feat(chat): implement legacy auto-login for chat sessions This commit introduces a LegacyAutoLogin component that prompts sign-in for legacy chat mounts without a bootstrap wrapper. The auto-login functionality is conditionally rendered based on the presence of a sessionId, enhancing user experience for legacy routes.
* feat(chat): consume session-scoped chat listing in sidebar
Pairs with the api change that pivots GET /api/chats to read
chats ⋈ sessions. Each row carries sessionId, so the sidebar pushes
the canonical /sessions/{sid}/chats/{cid} URL directly.
- getConversations maps the new camelCase wire fields onto the local
Conversation shape (topic ← title, account_id ← accountId,
updated_at ← updatedAt) plus the new sessionId field.
- useClickChat and the get_chats tool result component emit canonical
URLs.
- addOptimisticConversation takes sessionId — required for the row to
be addressable. Calls without a sessionId (legacy chats) skip
optimistic add and surface on the next refetch.
- useConversations drops the artist + org-artist filters: the new
listing doesn't carry artist linkage yet (returns after the artist
surface migration backfills sessions.artist_id and threads artistId
through POST /api/sessions).
- artist_id and memories become optional on Conversation — only the
optimistic-add path populates memories, and the new listing endpoint
doesn't carry artist_id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sidebar): scope conversations list to selectedArtist
api#626 wired up `?artist_account_id=` on `GET /api/chats` (filters
on `sessions.artist_id`) and started projecting `artistId` per row.
The sidebar wasn't reading either yet — without this change every
artist would see every chat once #1756 merges.
- `getConversations` accepts an optional `artistAccountId` and forwards
it as the `artist_account_id` query param; `ApiChatRow` now types
`artistId: string | null`, mapped onto `Conversation.artist_id`.
- `useConversations` pulls `selectedArtist?.account_id` from
`ArtistProvider` and includes it in the react-query key so switching
artists triggers a fresh fetch instead of returning the previous
artist's cached list.
- Drop the stale "linkage doesn't exist yet" comments in
`useConversations` and `types/Chat`.
- Tests cover the param being forwarded, omitted, and `artistId: null`
mapping to `undefined`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sidebar): hold chat list in loading until artists resolve
Without this, `useConversations` would fire an unfiltered
`GET /api/chats` while `useArtists` was still resolving the persisted
selection, then refetch once the artist landed — briefly flashing
every chat across artists in the sidebar before the correct list
swapped in.
- Gate the react-query call on `!isArtistsLoading` so the first
request always carries the correct `?artist_account_id`.
- Surface `isArtistsLoading || queryIsLoading` as the hook's
`isLoading` so `RecentChats` can render the existing skeleton
during the pre-resolve window (react-query's own `isFetching` is
false while the query is disabled, so the previous condition
would have rendered an empty list instead).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(chat): move getConversations into lib/chat/
Aligns with the repo convention (CLAUDE.md): "File Organization —
Domain-specific directories (e.g. /lib/fal/ not /lib/utils/fal.ts)".
- git mv lib/getConversations.tsx → lib/chat/getConversations.tsx
- git mv lib/__tests__/getConversations.test.ts → lib/chat/__tests__/getConversations.test.ts
- Updated the one importer (hooks/useConversations.tsx) and the
test's relative consts import.
No behavior change; tests + imports verified.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(chat): validate GET /api/chats response with zod
Previously the response body was decoded via a TypeScript type cast
(`{ chats?: ApiChatRow[] }`) — purely compile-time, zero runtime check.
A malformed row (missing/wrongly-typed field) would sail through and
land in the UI as `undefined`, with a confusing downstream crash.
Define a zod schema mirroring the docs#227 `ChatRoom` contract and
parse the response at the boundary. If the api ever drifts from the
documented shape, we get a clean error at the entry point and fall
back to `[]` rather than poisoning the UI state.
No behavior change on the happy path — all 10 existing tests still
pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Sweets Sweetman <sweetmantech@gmail.com>
Repoints sidebar rename and delete at the session-scoped api so they
operate on the same `chats` / `sessions` rows the workflow surface
persists into — the previous handlers hit the legacy `rooms`-shaped
endpoints and silently no-op'd on workflow chats.
- Rename: `PATCH /api/chats { chatId, topic }` → `PATCH /api/sessions/{sid}/chats/{cid} { title }`. `useRenameModal` and `updateChat` thread `sessionId` from the `Conversation` row and switch the wire field to `title`.
- Delete: switch from row-delete to session archive (`PATCH /api/sessions/{sid} { status: "archived" }`). The api filters archived sessions out of `GET /api/chats` (recoupable/api#630), so the chat disappears from the sidebar; archive also runs the existing `stopSandboxOnArchive` lifecycle, and the whole thing stays reversible from the admin side.
- Sidesteps the previous "Cannot delete the only chat in a session" 400, which was a real footgun once every workflow chat ended up in a 1-chat session.
- Removes the now-unused `lib/chats/deleteChat.ts`; adds `lib/sessions/archiveSession.ts` for the new wire call.
If a session ever has >1 chat, archiving from one row will hide the
others too. Every sidebar row maps 1:1 to its own session today
(each `POST /api/sessions` mints a session + single chat), so this is
a no-op in practice — worth knowing before any future flow creates
additional chats inside an existing session.
Co-authored-by: Arpit Gupta <arpitgupta1214@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: remove useAutoLogin from multiple components This commit removes the useAutoLogin hook from CatalogSongsPage, CatalogsPage, TasksPage, and Chat components, streamlining the codebase by eliminating unnecessary calls. The UserProvider component now includes useAutoLogin to manage user authentication more effectively. * refactor: integrate useAutoLogin into UserProvider for improved user authentication This commit moves the useAutoLogin hook into the UserProvider component, ensuring that user authentication is managed more effectively. A new UserAutoLogin component is introduced to encapsulate the auto-login logic, allowing the UserProvider to provide real user state to its consumers. * refactor(chat): remove redundant LegacyAutoLogin wrapper `LegacyAutoLogin` was added by chat#1760 as scaffolding to call `useAutoLogin()` from `<Chat>` when no `sessionId` is provided (i.e. the legacy `/chat/[roomId]` route). Now that this PR moves `useAutoLogin` into `<UserProvider>` via `<UserAutoLogin />`, the hook fires app-wide on every authenticated route — including legacy chat mounts. The wrapper is dead code. Also fixes a latent compile error introduced by the branch update: the merge picked up `LegacyAutoLogin` from chat#1760 but this PR's diff removed the `useAutoLogin` import from chat.tsx, leaving an unimported reference. Removing the wrapper resolves both. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(chat): remove redundant useAutoLogin from NewChatBootstrap Now that `<UserAutoLogin />` lives inside `UserProvider` and fires app-wide on every authenticated route, the per-component `useAutoLogin()` call here is dead — `NewChatBootstrap` is always mounted inside `UserProvider` (via `Providers.tsx`), so the centralized hook already covers it. Net effect: `useAutoLogin` now has exactly one call site (`providers/UserProvder.tsx`), eliminating the per-mount duplicate firings the hook used to guard against via `hasTriedLogin.current`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sweetman.eth <sweetmantech@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1759) * feat(chat): stamp sessions with selected artist; refactor useArtists Tags every newly-provisioned session with `selectedArtist.account_id` via `POST /api/sessions { artistId }` (api#628 already accepts it), so the sidebar's artist filter (#1756) groups chats under the right artist. Each artist switch mints a fresh session under the new context. The hard part wasn't passing the field — it was making the bootstrap fire exactly once per (artistId, orgId). Earlier iterations stacked refs on top of `useState` to police a duplicate POST that fired because `useArtists` resolved `selectedArtist` across two renders (commit A: `isLoading` flips false with `selectedArtist=null`; commit B: saved selection restored). Both commits independently satisfied the bootstrap's effect gate, both started a POST, both wrote to `sessions.artist_id`. Refs guarded the symptom; the real fix is to make `selectedArtist` resolve in a single render. ### `useArtists` — react-query + derived selection - Roster comes from `useQuery`, keyed on `(accountId, orgId)`. No imperative `getArtists` effect, no `setIsLoading` lifecycle. - `selectedArtist` is a single `useMemo` deriving from `(artists, savedSelections, orgKey, userOverride)`. Precedence: user override (this session) → saved (localStorage, per-org) → artists[0] auto-pick → null. - The race is gone *by construction*. There is no longer a render where `artists` is populated but `selectedArtist` is still null. - `useInitialArtists` deleted — its restoration effect folds into the memo; the sync-on-org-change effect is implicit (memo recomputes when `orgKey` changes). - `setArtists` mirrors React's `setState` signature (array | updater) so `useArtistPinToggle` and `useDeleteArtist` keep working without changes; under the hood it's `queryClient.setQueryData`. - `getArtists(artistId?)` becomes `queryClient.fetchQuery` + an optional `setSelectedArtist` post-fetch, preserving the existing call sites in `useArtistPinToggle`, `useDeleteArtist`, the `CreateArtistToolResult` / `UpdateArtistInfoSuccess` / `UpdateArtistSocialsSuccess` tool result components, and `saveSetting`. ### `useNewChatBootstrap` — useMutation, no refs - POST is semantically a mutation. `useMutation` owns the in-flight / success / error state instead of a hand-rolled state machine. - Effect gates on `mutation.variables` (react-query's own last-mutated args) plus `isPending` / `isSuccess`. Incidental re-renders re-enter the effect but bail idempotently. No refs. - Artist or org change → mutation.variables no longer matches → fresh `mutate()` fires under the new context. Orphan session from the old context is accepted (rare in practice). ### `Artist.tsx` — narrow the hard-nav - Hard-navigates to `/chat` only when switching artists from a tagged chat URL (`/sessions/{sid}/chats/{cid}` or legacy `/chat/{roomId}`). On bare `/chat` the bootstrap re-fires in place when `artistId` changes — no nav, no remount flicker. - `window.location.href` (not `router.replace`) because `useVercelChat`'s `silentlyUpdateUrl` writes via `history.replaceState`, leaving Next's internal router state out of sync with the URL bar — a client-side replace can no-op. ### `lib/sessions/createSession.ts` - `CreateSessionInput` gains optional `artistId`. Wire-compatible with `api/lib/sessions/validateCreateSessionBody.ts`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(chat): extract artist selection + chat-session provisioning per OCP Addresses sweetmantech's review comments on lines `useArtists.tsx:80` and `useNewChatBootstrap.ts:107` — net-new logic was being added inline to existing hooks instead of being given its own module. Two extractions: 1. `hooks/artists/useArtistSelection.ts` — owns the per-org artist selection: `useLocalStorage` for saved picks, `userOverride` state for explicit this-session pick/deselect, and the derived `selectedArtist` memo (precedence: override → saved → first). `useArtists.tsx` now just calls it with `(orgKey, artists)`. 2. `lib/sessions/provisionChatSession.ts` — combines `createSession` + `createSandbox` into one function. `useNewChatBootstrap.ts` now wraps that in `useMutation` and owns the trigger / input-change re-fire logic only. No behavior change — both hooks return the same shape and respond to the same inputs. Just smaller files with clearer single responsibility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): throw on missing Privy token in useArtists queryFn Addresses CodeRabbit feedback on `hooks/useArtists.tsx:61`. The previous shape silently resolved to `[]` when `getAccessToken()` came back null transiently — `useQuery` marked the query successful, `isLoading` flipped false, and `useNewChatBootstrap` saw a "settled, empty roster" and POSTed a session with `artistId: undefined`. Throw so the query goes to `isError` and the bootstrap stays gated. Same fix on the imperative `getArtists` callback for consistency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(chat): extract roster + provisioner hooks per OCP Two more OCP extractions, addressing the remaining inline-net-new-code comments on `useArtists.tsx` and `useNewChatBootstrap.ts`: 1. `hooks/artists/useArtistsRoster.ts` — owns the react-query fetch keyed on `(userId, orgId)`, the `setArtists` optimistic-update helper, and the `refetchArtists` imperative refetch. `useArtists` now consumes it via `useArtistsRoster({ userId, orgId })`. 2. `hooks/sessions/useProvisionChatSession.ts` — owns the mutation lifecycle: the `useMutation` setup, the `useEffect` with `sameInputs` guard, and the state-mapping from mutation state to the discriminated `ProvisionChatSessionState`. `useNewChatBootstrap` shrinks to a thin provider-wiring shim (~30 LOC). Each existing hook now has a single clear responsibility: - `useArtists` composes roster + selection + per-artist settings - `useNewChatBootstrap` wires Privy/Org/Artist providers into the provisioner No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): gate useArtistsRoster on authenticated, drop the throw Matches `useConversations` / `useCredits` — they gate the query on Privy's `authenticated` rather than throwing inside the queryFn for the transient-null-token case. Now this hook does the same: the query won't fire until Privy is ready, and if the token is still null in some pathological case the network call 401s and the query goes to `isError` (same outcome the throw produced). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): use isPending in useArtistsRoster so disabled-query window gates the bootstrap E2E found two POSTs on initial mount: one with `artist_id: null`, one with `artist_id: <saved-selection>`. Cause: in react-query v5 `isLoading` is `isPending && isFetching` — it's false when the query is disabled. While `useArtistsRoster` waits for `userData` from `UserProvider` to land, `enabled: false`, `isLoading: false`. The bootstrap effect saw "settled, empty roster" and fired POST #1 with `artistId: undefined`. Once `userData` arrived, the query enabled, artists resolved, selection moved to the saved artist, and the mutation effect re-fired with the real `artistId` (POST #2). Switch to `isPending` (true while disabled OR fetching) so the loading flag stays true through the entire "we don't know the roster yet" window. Surfaced as `isLoading` to keep the consumer interface stable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Sweets Sweetman <sweetmantech@gmail.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 39 minutes and 5 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (42)
✨ 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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4fad683bfd
ℹ️ 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".
| if (!sessionId || !chatId) { | ||
| setIsLoading(false); | ||
| return; |
There was a problem hiding this comment.
Preserve legacy room message loading
For legacy /chat/<roomId> pages, app/chat/[roomId]/page.tsx still renders <Chat id={roomId} /> without a sessionId, and useVercelChat still documents that missing sessionId should fall back to the legacy chat path. With this guard, the loader is called as useMessageLoader(undefined, roomId, ...) and immediately returns, so any not-yet-backfilled/deep-linked legacy chat opens with an empty conversation instead of fetching its persisted messages.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
5 issues found across 46 files
Confidence score: 3/5
- There is concrete regression risk in
lib/sessions/provisionChatSession.ts: if sandbox provisioning fails, session/chat rows can remain active and orphaned, which can create inconsistent state for users. hooks/artists/useArtistsRoster.tsandhooks/useConversations.tsxboth have user-facing behavior risks (invalid Bearer token when token is empty, and missing local artist scoping that can surface cross-artist chats).hooks/useMessageLoader.tsappears to break message loading on legacy/chat/[roomId]routes because missingsessionIdtriggers an early return, so this is not just a cosmetic issue.- Pay close attention to
lib/sessions/provisionChatSession.ts,hooks/artists/useArtistsRoster.ts,hooks/useConversations.tsx,hooks/useMessageLoader.ts, andproviders/UserProvder.tsx- they contain the main data consistency, auth/header validation, chat scoping/loading, and auto-login trigger risks before merge.
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="lib/sessions/provisionChatSession.ts">
<violation number="1" location="lib/sessions/provisionChatSession.ts:42">
P2: Sandbox provisioning failure is not compensated, so failed bootstrap can leave orphaned active session/chat rows.</violation>
</file>
<file name="hooks/artists/useArtistsRoster.ts">
<violation number="1" location="hooks/artists/useArtistsRoster.ts:57">
P2: The roster query sends an invalid Bearer token when `getAccessToken()` is empty because the value is force-cast instead of validated.</violation>
</file>
<file name="providers/UserProvder.tsx">
<violation number="1" location="providers/UserProvder.tsx:18">
P2: Auto-login is now mounted globally and triggers on `!email` only, which can incorrectly prompt login for wallet-authenticated users without an email.</violation>
</file>
<file name="hooks/useConversations.tsx">
<violation number="1" location="hooks/useConversations.tsx:49">
P2: Conversations are returned without local artist scoping, so an unfiltered fetch can expose cross-artist chats in the sidebar when no artist is selected.</violation>
</file>
<file name="hooks/useMessageLoader.ts">
<violation number="1" location="hooks/useMessageLoader.ts:24">
P2: Legacy `/chat/[roomId]` pages render `<Chat id={roomId} />` without a `sessionId`. Since `useVercelChat` passes `sessionId` as the first argument here, the `!sessionId` check causes an early return — no messages are ever loaded for legacy rooms. These chats will open with an empty conversation instead of fetching their persisted history. Consider allowing the load when `sessionId` is absent but `chatId` is present (falling back to the old fetch path).</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| { artistId: input.artistId, organizationId: input.orgId }, | ||
| accessToken, | ||
| ); | ||
| await createSandbox(session.cloneUrl, session.id, accessToken); |
There was a problem hiding this comment.
P2: Sandbox provisioning failure is not compensated, so failed bootstrap can leave orphaned active session/chat rows.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sessions/provisionChatSession.ts, line 42:
<comment>Sandbox provisioning failure is not compensated, so failed bootstrap can leave orphaned active session/chat rows.</comment>
<file context>
@@ -0,0 +1,44 @@
+ { artistId: input.artistId, organizationId: input.orgId },
+ accessToken,
+ );
+ await createSandbox(session.cloneUrl, session.id, accessToken);
+ return { sessionId: session.id, chatId: chat.id };
+}
</file context>
|
|
||
| const queryFn = useCallback(async () => { | ||
| const accessToken = await getAccessToken(); | ||
| return fetchArtists(accessToken as string, orgId); |
There was a problem hiding this comment.
P2: The roster query sends an invalid Bearer token when getAccessToken() is empty because the value is force-cast instead of validated.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/artists/useArtistsRoster.ts, line 57:
<comment>The roster query sends an invalid Bearer token when `getAccessToken()` is empty because the value is force-cast instead of validated.</comment>
<file context>
@@ -0,0 +1,87 @@
+
+ const queryFn = useCallback(async () => {
+ const accessToken = await getAccessToken();
+ return fetchArtists(accessToken as string, orgId);
+ }, [getAccessToken, orgId]);
+
</file context>
| return <UserContext.Provider value={value}>{children}</UserContext.Provider>; | ||
| return ( | ||
| <UserContext.Provider value={value}> | ||
| <UserAutoLogin /> |
There was a problem hiding this comment.
P2: Auto-login is now mounted globally and triggers on !email only, which can incorrectly prompt login for wallet-authenticated users without an email.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At providers/UserProvder.tsx, line 18:
<comment>Auto-login is now mounted globally and triggers on `!email` only, which can incorrectly prompt login for wallet-authenticated users without an email.</comment>
<file context>
@@ -12,9 +13,20 @@ const UserProvider = ({ children }: { children: React.ReactNode }) => {
- return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
+ return (
+ <UserContext.Provider value={value}>
+ <UserAutoLogin />
+ {children}
+ </UserContext.Provider>
</file context>
| <UserAutoLogin /> | |
| {!user.email && !user.address ? <UserAutoLogin /> : null} |
| // Fallback: no artists in org (shouldn't happen normally) | ||
| return fetchedConversations; | ||
| }, [selectedArtist, fetchedConversations, orgArtistIds]); | ||
| const conversations = fetchedConversations; |
There was a problem hiding this comment.
P2: Conversations are returned without local artist scoping, so an unfiltered fetch can expose cross-artist chats in the sidebar when no artist is selected.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useConversations.tsx, line 49:
<comment>Conversations are returned without local artist scoping, so an unfiltered fetch can expose cross-artist chats in the sidebar when no artist is selected.</comment>
<file context>
@@ -2,79 +2,75 @@ import { useMemo } from "react";
- // Fallback: no artists in org (shouldn't happen normally)
- return fetchedConversations;
- }, [selectedArtist, fetchedConversations, orgArtistIds]);
+ const conversations = fetchedConversations;
- // Optimistic update helpers for creating a new chat room
</file context>
|
|
||
| useEffect(() => { | ||
| if (!roomId) { | ||
| if (!sessionId || !chatId) { |
There was a problem hiding this comment.
P2: Legacy /chat/[roomId] pages render <Chat id={roomId} /> without a sessionId. Since useVercelChat passes sessionId as the first argument here, the !sessionId check causes an early return — no messages are ever loaded for legacy rooms. These chats will open with an empty conversation instead of fetching their persisted history. Consider allowing the load when sessionId is absent but chatId is present (falling back to the old fetch path).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useMessageLoader.ts, line 24:
<comment>Legacy `/chat/[roomId]` pages render `<Chat id={roomId} />` without a `sessionId`. Since `useVercelChat` passes `sessionId` as the first argument here, the `!sessionId` check causes an early return — no messages are ever loaded for legacy rooms. These chats will open with an empty conversation instead of fetching their persisted history. Consider allowing the load when `sessionId` is absent but `chatId` is present (falling back to the old fetch path).</comment>
<file context>
@@ -1,26 +1,27 @@
useEffect(() => {
- if (!roomId) {
+ if (!sessionId || !chatId) {
setIsLoading(false);
return;
</file context>
Integration verification — full bundle on
|
| Step | Source PR | Result |
|---|---|---|
| Anonymous load → Privy modal opens; sign-in completes | #1761 | ✅ <UserAutoLogin /> in UserProvider fires app-wide |
Land on / (HomePage) → bootstrap chain fires |
#1760 | ✅ <NewChatBootstrap> mounts, no spinner regression |
POST /api/sessions { artistId } stamps sessions.artist_id |
#1759 | ✅ verified in DB: session 8c736e25-… has artist_id = 1873859c-… (Gatsby Grace, the selected artist) |
POST /api/sandbox provisions Vercel Sandbox + repo |
api#618/#620 | ✅ |
| Send "PROD-CUTOVER-1766-integration-test: what files do you see?" | #1748 | ✅ went through POST /api/chat/workflow, not legacy /api/chat |
| Agent invokes glob + glob + bash tools and replies | sandbox connection | ✅ bash tools available, filesystem accessible |
URL canonicalizes mid-send to /sessions/8c736e25-.../chats/73b6012c-... |
#1757 | ✅ |
| New chat appears in sidebar (artist-filtered to Gatsby Grace) | #1756 + api#626 | ✅ — surfaces at top of 530-row Gatsby Grace listing |
Previously validated this session
The five chat PRs in this bundle were each tested on their own previews earlier today before merging to test. All verified:
- refactor(chat): remove UUID generation and update HomePage to use NewChatBootstrap #1760 —
/shows bash tools on send (was broken on baretestpre-refactor(chat): remove UUID generation and update HomePage to use NewChatBootstrap #1760) - feat(chat): consume session-scoped chat listing in sidebar #1756 — sidebar fires
GET /api/chats?artist_account_id=…server-side; artist switch triggers refetch - feat(chat): session-scoped sidebar rename + archive-on-delete #1763 — rename hits
PATCH /api/sessions/{sid}/chats/{cid} { title }; delete archives session → row vanishes - refactor: remove useAutoLogin from multiple components #1761 — auto-login centralized; anonymous landings on
/,/catalogs,/tasks,/chat/[roomId]all open the modal - feat(chat): stamp sessions with selected artist; refactor useArtists #1759 —
sessions.artist_idcorrectly stamped on new chats (verified again above against post-merge api)
Known accepted regressions (in PR body)
All still apply — no surprises from the integration testing:
- No MCP tools in workflow sandbox (
get_chats,send_email, music industry tools) - No artist context in system prompt
- No auto chat-title generation (title = first user message)
- Legacy
/chat/{roomId}URLs show empty greeting (chat history doesn't load via legacy path; canonical/sessions/{sid}/chats/{cid}URLs work fine) - Artist switch mid-chat provisions 3 sessions (one survives, two orphaned — known wasteful lifecycle, follow-up item)
CI status
| Check | Result |
|---|---|
| Run unit tests | ✅ |
| CodeRabbit | ✅ |
| Vercel – chat | ✅ |
| cubic | ✅ |
Recommendation
api#631 has shipped to recoup-api.vercel.app. The chat code now expects exactly that surface. Ready to merge — close the deployment ordering window ASAP so production chat catches up with production api.
Promotes the chat-workflow cutover from
testtomain. This is the chat half of the recoupable/chat#1747 bundle — pairs with recoupable/api#631 (must merge before this).What this ships
9 PRs merged to
testsince the cutover began:/api/chat/workflow(vertical-slice cutover)/sessions/[sid]/chats/[cid]route + session-scoped loader?artist_account_id=)silentlyUpdateUrlto emit canonical session URLsessions.artist_idfrom selectedArtist; refactoruseArtists/usesNewChatBootstrap(bash tools on/)useAutoLoginintoUserProvider(5 sites → 1)9 commits ahead of
main, 0 behind.What changes for the user
On
/and/chat:POST /api/sessions(session+chat row provisioned in api) +POST /api/sandbox(Vercel Sandbox spun up) +POST /api/chat/workflow(streaming response)/sessions/{sid}/chats/{cid}Sidebar:
?artist_account_id=(server-side filter)PATCH; delete archives the owning sessionLegacy
/chat/{roomId}URLs:Known accepted regressions
Documented in the chat#1747 issue body and verified during preview testing:
get_chats, no artist-data tools, nosend_email, no Composio integrations). The workflow agent gets bash + general file access only.<Chat>, but the lifecycle is wasteful. Follow-up after promotion./api/chats/{chatId}/artist404s on chats without arooms.artist_idrow — pre-existing endpoint issue, doesn't break functionalityOrdering requirement
Merge api#631 first. Wait for it to deploy to
https://recoup-api.vercel.app. Verify the new shape ships:Then merge this PR. The chat client calls endpoints that only exist in api#631:
GET /api/sessions/{sid}/chats/{cid}— history readerPATCH /api/sessions/{sid}/chats/{cid}— renamePATCH /api/sessions/{sid} {"status": "archived"}— deletePOST /api/sessions { artistId }— new chatGET /api/chats?artist_account_id=...— sidebar filterIf chat merges first, the sidebar breaks (wrong response shape), new chats fail, and rename/delete 404.
Rollback plan
Revert the merge commit. If chat ships but api hasn't (or api gets rolled back), users will see broken sidebar + broken new-chat flow until chat is also reverted.
Test plan
https://recoup-api.vercel.apphttps://chat.recoupable.com: load/, send a message, confirm URL canonicalizes to/sessions/{sid}/chats/{cid}and bash tools workhttps://chat.recoupable.com: sidebar lists only the selected artist's chats; switch artist → list updateshttps://chat.recoupable.com: rename a chat → title updates; delete a chat → row disappearsSummary by cubic
Routes new chats through recoup-api’s
POST /api/chat/workflowby bootstrapping a session and sandbox, and switches the app to canonical session-based chat URLs with an artist-scoped sidebar. Also upgrades theaiSDK stack to stable v6 for proper tool output rendering.New Features
POST /api/sessions→POST /api/sandbox→ stream viaPOST /api/chat/workflow(bash tools enabled)./sessions/[sessionId]/chats/[chatId]; URL updates after first send.GET /api/chats, server-filtered by?artist_account_id=; click navigates to the canonical URL.PATCH /api/sessions/{sid}/chats/{cid}; Delete: archive session viaPATCH /api/sessions/{sid} { status: "archived" }.useArtistsRoster+useArtistSelectionstabilize per-org selection and prevent duplicate bootstraps.sessionIdis present; legacy chats still post to/api/chat. Messages load viaGET /api/sessions/{sid}/chats/{cid}.useAutoLogincentralized inUserProvider.ai→6.0.165,@ai-sdk/react→3.0.167,@ai-sdk/anthropic→3.0.80,@ai-sdk/google→3.0.80,@ai-sdk/openai→3.0.66.Migration
https://recoup-api.vercel.app/api/chatsreturns chat rows with["accountId","artistId","id","sessionId","title","updatedAt"].POST /api/sessions,POST /api/sandbox,POST /api/chat/workflowGET /api/chats?artist_account_id=...GET /api/sessions/{sid}/chats/{cid},PATCH /api/sessions/{sid}/chats/{cid},PATCH /api/sessions/{sid}Written for commit 4fad683. Summary will update on new commits.