Skip to content

Promote chat-workflow cutover to main#1766

Merged
sweetmantech merged 9 commits into
mainfrom
test
Jun 1, 2026
Merged

Promote chat-workflow cutover to main#1766
sweetmantech merged 9 commits into
mainfrom
test

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Promotes the chat-workflow cutover from test to main. 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 test since the cutover began:

PR What
#1748 feat: route new chats through recoup-api /api/chat/workflow (vertical-slice cutover)
#1751 fix(deps): bump ai SDK off beta to stable v6 so workflow tool outputs render
#1752 feat(chat): add /sessions/[sid]/chats/[cid] route + session-scoped loader
#1756 feat(chat): consume session-scoped chat listing in sidebar (artist filter via ?artist_account_id=)
#1757 feat(chat): rewrite silentlyUpdateUrl to emit canonical session URL
#1759 feat(chat): stamp sessions.artist_id from selectedArtist; refactor useArtists
#1760 refactor(chat): HomePage / uses NewChatBootstrap (bash tools on /)
#1761 refactor: centralize useAutoLogin into UserProvider (5 sites → 1)
#1763 feat(chat): session-scoped sidebar rename + archive-on-delete

9 commits ahead of main, 0 behind.

What changes for the user

On / and /chat:

  • New chats go through POST /api/sessions (session+chat row provisioned in api) + POST /api/sandbox (Vercel Sandbox spun up) + POST /api/chat/workflow (streaming response)
  • The agent has bash tools available — files, grep, code execution all work
  • URL canonicalizes on first send: /sessions/{sid}/chats/{cid}

Sidebar:

  • Lists are scoped to the selected artist via ?artist_account_id= (server-side filter)
  • Click a chat row → navigates to canonical URL
  • Rename hits session-scoped PATCH; delete archives the owning session

Legacy /chat/{roomId} URLs:

  • Still resolve (route preserved), but show empty default greeting instead of message history because the new reader requires a sessionId
  • Step 1 of chat#1747 will tighten this to a clean 404 in a follow-up

Known accepted regressions

Documented in the chat#1747 issue body and verified during preview testing:

  • MCP tools removed from the workflow sandbox (no get_chats, no artist-data tools, no send_email, no Composio integrations). The workflow agent gets bash + general file access only.
  • No automatic chat title generation — chat title is the first user message verbatim
  • No artist context in system prompt — agent doesn't know which artist the chat is about
  • No Telegram new-conversation notifications (re-scoped into a future spend-digest cron)
  • Artist switch mid-chat provisions 3 sessions instead of 1 — flagged in chat#1759 comment; only the final session is used by <Chat>, but the lifecycle is wasteful. Follow-up after promotion.
  • /api/chats/{chatId}/artist 404s on chats without a rooms.artist_id row — pre-existing endpoint issue, doesn't break functionality

Ordering requirement

Merge api#631 first. Wait for it to deploy to https://recoup-api.vercel.app. Verify the new shape ships:

curl -s -H "x-api-key: ..." "https://recoup-api.vercel.app/api/chats" | jq '.chats[0] | keys'
# Expect: ["accountId", "artistId", "id", "sessionId", "title", "updatedAt"]

Then merge this PR. The chat client calls endpoints that only exist in api#631:

  • GET /api/sessions/{sid}/chats/{cid} — history reader
  • PATCH /api/sessions/{sid}/chats/{cid} — rename
  • PATCH /api/sessions/{sid} {"status": "archived"} — delete
  • POST /api/sessions { artistId } — new chat
  • GET /api/chats?artist_account_id=... — sidebar filter

If 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

  • api#631 merged and deployed to https://recoup-api.vercel.app
  • Verify api response shape on prod (curl above)
  • CI green on this PR's merge commit
  • On https://chat.recoupable.com: load /, send a message, confirm URL canonicalizes to /sessions/{sid}/chats/{cid} and bash tools work
  • On https://chat.recoupable.com: sidebar lists only the selected artist's chats; switch artist → list updates
  • On https://chat.recoupable.com: rename a chat → title updates; delete a chat → row disappears

Summary by cubic

Routes new chats through recoup-api’s POST /api/chat/workflow by bootstrapping a session and sandbox, and switches the app to canonical session-based chat URLs with an artist-scoped sidebar. Also upgrades the ai SDK stack to stable v6 for proper tool output rendering.

  • New Features

    • New-chat flow: POST /api/sessionsPOST /api/sandbox → stream via POST /api/chat/workflow (bash tools enabled).
    • Canonical route: /sessions/[sessionId]/chats/[chatId]; URL updates after first send.
    • Sidebar: lists from GET /api/chats, server-filtered by ?artist_account_id=; click navigates to the canonical URL.
    • Rename: PATCH /api/sessions/{sid}/chats/{cid}; Delete: archive session via PATCH /api/sessions/{sid} { status: "archived" }.
    • Artist UX: new useArtistsRoster + useArtistSelection stabilize per-org selection and prevent duplicate bootstraps.
    • Transport: uses workflow path when sessionId is present; legacy chats still post to /api/chat. Messages load via GET /api/sessions/{sid}/chats/{cid}.
    • Auth: useAutoLogin centralized in UserProvider.
    • Dependencies: upgrade ai6.0.165, @ai-sdk/react3.0.167, @ai-sdk/anthropic3.0.80, @ai-sdk/google3.0.80, @ai-sdk/openai3.0.66.
  • Migration

    • Merge Promote chat-workflow cutover stack to main api#631 first and wait for deploy. Verify: GET https://recoup-api.vercel.app/api/chats returns chat rows with ["accountId","artistId","id","sessionId","title","updatedAt"].
    • Chat depends on new endpoints and shapes:
      • POST /api/sessions, POST /api/sandbox, POST /api/chat/workflow
      • GET /api/chats?artist_account_id=...
      • GET /api/sessions/{sid}/chats/{cid}, PATCH /api/sessions/{sid}/chats/{cid}, PATCH /api/sessions/{sid}
    • If chat lands before the API, new chats, sidebar, and rename/delete will fail. Rollback by reverting this merge.

Written for commit 4fad683. Summary will update on new commits.

Review in cubic

sweetmantech and others added 9 commits May 27, 2026 12:54
* 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>
@vercel

vercel Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview Jun 1, 2026 7:15pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@sweetmantech, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 717c429f-d81e-4ffe-99ec-e4e589f5cab4

📥 Commits

Reviewing files that changed from the base of the PR and between 464ccb7 and 4fad683.

⛔ Files ignored due to path filters (4)
  • lib/chat/__tests__/getConversations.test.ts is excluded by !**/*.test.* and included by lib/**
  • package.json is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
  • types/Chat.tsx is excluded by none and included by none
📒 Files selected for processing (42)
  • app/chat/page.tsx
  • app/page.tsx
  • app/sessions/[sessionId]/chats/[chatId]/page.tsx
  • components/Catalog/CatalogSongsPage.tsx
  • components/Catalog/CatalogsPage.tsx
  • components/Header/Artist.tsx
  • components/Home/HomePage.tsx
  • components/Sidebar/Modals/DeleteConfirmationModal.tsx
  • components/Sidebar/RecentChats/RecentChats.tsx
  • components/TasksPage/TasksPage.tsx
  • components/VercelChat/NewChatBootstrap.tsx
  • components/VercelChat/chat.tsx
  • components/VercelChat/tools/chats/GetChatsResult.tsx
  • hooks/artists/useArtistSelection.ts
  • hooks/artists/useArtistsRoster.ts
  • hooks/sessions/useProvisionChatSession.ts
  • hooks/useArtists.tsx
  • hooks/useAutoLogin.tsx
  • hooks/useChatTransport.ts
  • hooks/useClickChat.tsx
  • hooks/useConversations.tsx
  • hooks/useDeleteChat.ts
  • hooks/useInitialArtists.tsx
  • hooks/useMessageLoader.ts
  • hooks/useNewChatBootstrap.ts
  • hooks/useRenameModal.ts
  • hooks/useVercelChat.ts
  • lib/chat/getConversations.tsx
  • lib/chats/deleteChat.ts
  • lib/chats/updateChat.ts
  • lib/getConversations.tsx
  • lib/messages/getChatMessages.ts
  • lib/sandboxes/SandboxCreateRequestError.ts
  • lib/sandboxes/createSandbox.ts
  • lib/sandboxes/getFallbackSandboxCreateErrorMessage.ts
  • lib/sandboxes/parseCreateSandboxErrorResponse.ts
  • lib/sessions/archiveSession.ts
  • lib/sessions/createSession.ts
  • lib/sessions/provisionChatSession.ts
  • lib/string/getOptionalString.ts
  • providers/UserProvder.tsx
  • providers/VercelChatProvider.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test

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.

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

Copy link
Copy Markdown

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: 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".

Comment thread hooks/useMessageLoader.ts
Comment on lines +24 to 26
if (!sessionId || !chatId) {
setIsLoading(false);
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.

P1 Badge 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 👍 / 👎.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.ts and hooks/useConversations.tsx both 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.ts appears to break message loading on legacy /chat/[roomId] routes because missing sessionId triggers 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, and providers/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);

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: 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);

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

Comment thread providers/UserProvder.tsx
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
return (
<UserContext.Provider value={value}>
<UserAutoLogin />

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: 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>
Suggested change
<UserAutoLogin />
{!user.email && !user.address ? <UserAutoLogin /> : null}

// Fallback: no artists in org (shouldn't happen normally)
return fetchedConversations;
}, [selectedArtist, fetchedConversations, orgArtistIds]);
const conversations = fetchedConversations;

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

Comment thread hooks/useMessageLoader.ts

useEffect(() => {
if (!roomId) {
if (!sessionId || !chatId) {

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

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Integration verification — full bundle on chat-git-test-recoup.vercel.app against the now-promoted api

Tested with Chrome DevTools after merging api#631 to main. This validates that all 9 chat PRs in this bundle work together against the upgraded api surface.

End-to-end smoke (single user journey)

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:

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.

@sweetmantech sweetmantech merged commit e856958 into main Jun 1, 2026
4 checks passed
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