Skip to content

Post-cutover cleanup + follow-up items #1767

Description

@sweetmantech

Tracking issue for cleanup work that follows the chat.recoupable.com → /api/chat/workflow cutover (chat#1747, shipped to prod on 2026-06-01). The cutover is fully live on chat.recoupable.com and recoup-api.vercel.app; this issue captures the remaining items that didn't need to ship with the bundle.

Goal

Delete the legacy /api/chat endpoint in api and rename /api/chat/workflow/api/chat so the workflow path reclaims the canonical name. Downstream: lib/chat/handleChatStream.ts + every other reader of rooms/memories deleted, those tables dropped, the chat client's useChatTransport legacy fork and app/chat/[roomId]/page.tsx removed, and the known UX rough edges (bootstrap spinner wait, artist-switch lifecycle, dead /api/chats/{chatId}/artist 404s) smoothed.

What shipped (context)

  • api/main has 23 commits / 13 PRs of workflow + sessions + chats + backfill + archive-filter changes (merged via api#631 as 31c9d58b)
  • chat/main has 9 commits / 9 PRs of NewChatBootstrap + canonical URL + sidebar + artist-stamping + auto-login centralization (merged via chat#1766 as e8569589)
  • prod data: Phase 2 backfill + straggler backfill (6 additional rooms, 16 messages) all migrated cleanly to sessions/chats/chat_messages; 17,991 sessions with correct artist_id; 0 migratable stragglers remaining

Done

  • PR cleanup — closed chat#1754 (superseded; chat-side direct-DB approach was wrong layer) and chat#1755 (Approach A — pivoted to the incremental cutover) with rationale comments on each.

  • chat#1765 — drop legacy useChatTransport branch + delete app/chat/[roomId]/page.tsx. ✅ Shipped 2026-06-03 (merged to test, then testmain; main and test synced). sessionId is now required through useVercelChat / VercelChatProvider / Chat; the if(!sessionId) legacy /api/chat fork in useChatTransport is gone (transport always targets /api/chat/workflow); app/chat/[roomId]/page.tsx deleted so legacy /chat/{id} URLs return 404 (branded not-found page). Also: every roomId useParams reader migrated to chatId, the two URL producers (useCreateArtistTool, generateTxtFileEmail) now emit session-scoped /sessions/{id}/chats/{id} URLs, and lib/chat/chatPaths was split one-function-per-file per SRP. Verified end-to-end on preview: new chat → session-scoped URL, transport POSTs /api/chat/workflow with sessionId+chatId+bearer, assistant streams, legacy route 404s.

  • chat#1774 — defer new-chat bootstrap wait from spinner to send button. ✅ Shipped 2026-06-03 (merged to test, then testmain via chat#1775; main/test synced). Chose option A: NewChatBootstrap renders <Chat> immediately with a typeable input + stable placeholder chat id, provisions session + sandbox in parallel, and gates Send until the api-minted sessionId+chatId land. The transport reads those ids via refs so the first send carries the real values (useChat pins the mount-time transport, so a memo rebuild alone wasn't enough). The flashing cue was replaced by a workspace status dot (red/yellow/green + tooltip; own WorkspaceStatusIndicator component, SRP) in the input's top-right. Measured the wait first per this item's note: POST /api/sandbox10.9s cold is the dominant cost (api-side), so this ships the typeable-while-provisioning UX as the interim fix; the real latency fix is the non-blocking-sandbox work below (don't block the first token on the sandbox). Closed chat#1764 (stale option-A WIP) and chat#1773 (option B — app-level eager provisioning + draft shell) as superseded; verified end-to-end on preview.

  • Tighten artist-switch lifecycle (was: "currently mints 3 sessions per click"). ✅ Already fixed on main by chat#1759's merge — no separate follow-up fix was needed. The 3× was flagged on a feat(chat): stamp sessions with selected artist; refactor useArtists #1759 preview SHA (4d26b488, flag comment) that never merged and still used react-query's isLoading; the merged version switched the roster gate to isPending (source: useArtistsRoster.ts L60-86), so the bootstrap stays gated until the roster is populated and selectedArtist never fires as null (with useArtistSelection's single-render useMemo). Verified on prod: each switch fires 1 POST /api/sessions + 1 POST /api/sandbox (tested personal + org/heavy-repo + 4× CPU/Slow-4G throttle, all switch paths incl. the hard-nav reload). chat#1769 was the follow-up PR but is redundant for the 3× bug — it remains optional defensive hardening for the in-flight-mismatch edge. Full analysis: chat#1769 comment.

  • Credit-spend visibility digest (Telegram). ✅ Shipped 2026-06-03 — api#634testmain (api#637). Vercel cron */10 * * * *GET /api/internal/credit-spend-digestget_credit_spend_digest (database#28, live in prod) → Telegram top-spenders summary; empty window no-ops; CRON_SECRET-gated. Verified end-to-end on preview (401/200 auth, empty no-op, real digest posted to Telegram). Surfaced the model-selector bug tracked below.

  • chat#1768 — clean up dead GET /api/chats/{chatId}/artist 404 spam. ✅ Shipped 2026-06-03 (squash-merged to test, then testmain via chat#1778; main/test synced). Deleted the legacy caller (useArtistFromRoom + lib/chats/getChatArtist.ts); the active artist is now resolved from the session — useArtistFromChat reads GET /api/sessions/{sessionId}.artistId (new client wrapper lib/sessions/getSessionById.ts, zod-validated at the boundary) and selects the matching roster artist. No new endpoint added to the chat repo — uses the existing recoup-api session endpoint. Added shared lib/api/safeJsonParse.ts (DRY, not in utils/). Verified end-to-end on preview via Chrome DevTools: opening a chat fires GET /api/sessions/{id} (returns artistId) and zero GET /api/chats/{id}/artist calls; cross-artist switch confirmed (Bieber→Alex Paul: GET /api/sessions/{id}GET /api/chats?artist_account_id=… for the new artist), re-verified on the final build. Edge cases that correctly no-op (not regressions): session artist_id null (~24% of sessions) and chats whose artist isn't in the user's roster.

  • Retire the legacy POST /api/chat streaming endpoint (api#632 + docs#230). ✅ Shipped 2026-06-04. Decision: deleted outright (no deprecate-with-comms) — after chat#1765 no in-monorepo caller remained and /api/chat/workflow fully covers the UI contract. api#632 (squash → test as c0eb5496, then testmain via api#643 as 5e31ad06; test re-synced with main): deleted app/api/chat/route.ts, lib/chat/handleChatStream.ts, lib/chat/handleChatCompletion.ts, lib/emails/handleSendEmailToolOutputs.ts + their tests, and trimmed chatEndToEnd.test.ts to the still-live validation/setup/credits paths. A follow-up dead-code sweep on the same PR removed 4 modules the legacy handlers were the sole reachers of: lib/emails/extractSendEmailResults.ts; the dead duplicate lib/messages/validateMessages.ts (the live one is lib/chat/validateMessages.ts); lib/supabase/memories/upsertMemory.ts (+ its now-vestigial vi.mock); and the pre-existing orphan lib/messages/getLatestUserMessageText.ts (+ test). A full transitive trace confirmed everything else the legacy handlers touched (setupConversation/createNewRoom/generateChatTitle/setupChatRequest/getGeneralAgent/rooms+memories readers) is still reachable from live entry points (/api/chat/generate, /api/chats, Slack, email-inbound, MCP create-artist), so it correctly stays for Phase 4. Net +26 / −1435. docs#230 (squash → main as b0eaaee): removed the endpoint page, its OpenAPI entries, and the nav link. Shared helpers used by /api/chat/workflow, /api/chat/generate, the stop route, Slack, and email flows untouched. Verified: local lint:check clean + 692 tests pass; repo-wide grep shows 0 references to any deleted module; preview E2E on api-git-feat-remove-legacy-chat-endpoint-recoup.vercel.appPOST /api/chat404 (gone), POST /api/chat/workflow400 (validation wired), POST /api/chat/generate400, GET /api/chats401 (sibling module graphs load cleanly, not 500). Full rooms/memories table teardown remains in Phase 4 below.

  • Workflow billed the default model instead of the picked one — fixed (chat#1781). ✅ Shipped 2026-06-04 (squash → test as c711516f, then testmain via chat#1782 as 07cdc2f0; main/test synced). Root cause: the workflow reads the model from chats.model_id (api/lib/chat/handleChatWorkflowStream.ts:93), which the UI never wrote before the first send, so new chats billed the column default. Decision: follow the open-agents pattern — resolve server-side from the persisted chat.model_id, not the request body, so api#638 (thread model through the workflow body) was closed as wrong-layer. Fix (chat-only, no api/DB change): persist the picker selection to chats.model_id and await that PATCH right before the send — covering the first-turn (useVercelChat.handleSubmit), retry/edit (reloadregenerate), and append paths — gated by a pure shouldPersistChatModel guard; extracted into hooks/usePersistSelectedModel.ts per OCP review feedback. Reuses Enhance chat functionality by integrating model ID persistence and updating chat update parameters.  #1779's buildPatchChatBody + updateChat(modelId). Superseded chat#1779 (its PATCH-only sync hooks billed the localStorage default in the live repro; left open for its author to close). Verified end-to-end on the fix(chat): persist selected model before send so the workflow bills it #1781 preview (Chrome DevTools + Supabase usage_events): new chat → Claude Sonnet 4.6 → billed anthropic/claude-sonnet-4.6; then changed model → Retry → regenerated turn billed google/gemini-3-pro-preview; picked = billed on both paths. Unit tests green (shouldPersistChatModel, buildPatchChatBody); tsc + lint clean.

  • Renamed /api/chat/workflow/api/chat — canonical name reclaimed, shipped to prod. ✅ Cut over 2026-06-05. Done doc-first then a coordinated hard rename (no alias): docs #235 (4b80c1d2main) → api #645 (66bf07eftest) → chat #1783 (9798ef30test) → open-agents #47 (25468103main); then promoted in lockstep: open-agents#47, api #648 (4a0d70ecmain), chat #1784 (5089252amain); test re-synced with main on api + chat. What changed: added app/api/chat/route.ts (POST + OPTIONS → handleChatWorkflowStream) and deleted app/api/chat/workflow/route.ts; repointed chat useChatTransport and open-agents CHAT_API_POST_PATH to /api/chat; docs OpenAPI makes /api/chat canonical. Verified in prod: api.recoupable.com/api/chat → 400 (live), /api/chat/workflow404 (gone); chat.recoupable.com new chat → POST api.recoupable.com/api/chat200, streamed pong, zero /workflow calls; open-agents live E2E POST …/api/chat → 200, pong. Cutover note: a brief deploy-lag window occurred — chat prod (old build) briefly POSTed /api/chat/workflow → 404 until chat's new build deployed; expected for a hard rename with no alias, self-healed on deploy. Accepted leftover (cosmetic): a few api route/handler comments still say "backward-compatible alias" (those docstring edits never got committed — the functional change shipped correctly); optional tidy follow-up. ⚠️ External API-key callers must use /api/chat (no alias).

Open — pre-existing docs gaps (separate work, low priority)

Surfaced while verifying api#628 against docs#228. Neither was introduced by the cutover; tracked for visibility:

  • Session.isNewBranch is marked required in the OpenAPI schema but is missing from every actual response (POST/GET/PATCH /api/sessions). Either drop the required-tag or have the api always emit it.
  • PATCH /api/sessions/{id} has no documented request body schema (PatchSessionBody doesn't exist in the spec) even though the endpoint clearly accepts { title } (and now { status }). Small docs PR to add it.

Open — Phase 4 cleanup (LAST, only after the cutover bundle is stable on main for some time)

  • Phase 4 — legacy code cleanup. (Updated: lib/chat/handleChatStream.ts, the legacy app/api/chat/route.ts, and lib/chat/handleChatCompletion.ts were already deleted by api#632. Note app/api/chat/route.ts now exists again as the canonical workflow endpoint from the rename — do NOT delete it.) Remaining rooms/memories readers to remove: getGeneralAgent.ts, setupChatRequest.ts, setupToolsForRequest.ts, MCP/Composio plumbing — i.e. the chain still reachable from /api/chat/generate, Slack, and email-inbound. Confirms no production code path still depends on the legacy tables.

  • Drop legacy rooms + memories tables. Only after Phase 4 has removed the last rooms/memories readers. Requires a migration file in recoupable/database to drop them (plus FK-dependent tables like room_reports / segment_rooms / memory_emails as applicable). Destructive + irreversible — snapshot first and confirm nothing still reads rooms/memories before dropping.

Inherited gaps from open-agents cutover (still applies, tracked separately)

Tracked in recoupable/api#605. Not blocking; documented here for visibility:

  • Stop monitor — Stop button doesn't actually halt the model (chat#1770 + api#590 address this.)
  • Multi-tool-call traces broken client-side
  • Sandbox state persistence missing
  • Sub-agent (task tool) credit attribution missing
  • ask_user_question result persistence on device switch missing
  • workflow_runs telemetry missing

Accepted regressions (still in effect — re-add later as needed)

These shipped with the cutover and are still missing from production chat:

  • No MCP tools (artist data, music industry APIs, send_email) inside the workflow sandbox
  • No Composio tools (Sheets, Drive, Docs, TikTok)
  • No artist context in system prompt
  • No automatic chat title generation (title = first user message verbatim)
  • No Telegram new-conversation notifications (being re-scoped above)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions