Persistent AI Chat history #1296
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis pull request adds persistent AI conversation functionality to the Stack Companion. It introduces database tables for storing conversations and messages, backend API endpoints for conversation lifecycle management, frontend UI components for chat rendering and conversation browsing, and client-side hooks for API interactions. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User (Frontend)
participant ChatUI as Chat UI
participant API as Backend API
participant DB as Database
User->>ChatUI: Send message
ChatUI->>API: POST /ai-conversations/[id]/messages or POST /ai-conversations with initial messages
API->>DB: Create aiMessage or create aiConversation + aiMessages
DB-->>API: Record(s) created with id, timestamps
API-->>ChatUI: Streaming response (AI content)
ChatUI->>ChatUI: Word-by-word reveal via useWordStreaming
ChatUI->>API: PUT /ai-conversations/[id]/messages (persist assistant response)
API->>DB: Replace aiMessage records for conversation
DB-->>API: Success
API-->>ChatUI: 200 OK
ChatUI-->>User: Display persisted message history
sequenceDiagram
participant User as User (Frontend)
participant ListUI as Conversation List
participant API as Backend API
participant DB as Database
User->>ListUI: Open Stack Companion "Ask AI"
ListUI->>API: GET /ai-conversations?projectId=...
API->>DB: Query aiConversation filtered by projectUserId, projectId
DB-->>API: [{ id, title, projectId, updatedAt }, ...]
API-->>ListUI: Conversation summaries
ListUI-->>User: Render conversation list with titles and timestamps
User->>ListUI: Click conversation or delete button
ListUI->>API: DELETE /ai-conversations/[id] or GET /ai-conversations/[id]
API->>DB: Delete aiConversation (cascades to aiMessages) or select aiConversation + aiMessages
DB-->>API: Success or conversation detail
API-->>ListUI: 200 OK or conversation data
ListUI-->>User: Update list or open conversation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
Greptile SummaryThis PR adds persistent AI chat history to the Stack Companion sidebar: a new Key concerns found during review:
Confidence Score: 4/5Not safe to merge as-is due to a non-atomic delete+recreate that can permanently destroy conversation messages. Two P1 issues exist: the missing transaction in the messages PUT endpoint (confirmed data-loss path) and the race condition in the frontend save logic. The rest of the changes are well-structured and the authorization model is correct. apps/backend/src/app/api/latest/internal/ai-conversations/[conversationId]/messages/route.tsx (transaction), apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx (race condition + error handling) Important Files Changed
Sequence DiagramsequenceDiagram
participant UI as AIChatWidgetInner
participant API as Backend API
participant DB as Database
UI->>API: POST /ai/query/stream (messages)
API-->>UI: stream response (status: streaming)
API-->>UI: stream ends (status: ready)
UI->>UI: useEffect detects streaming→ready
Note over UI: isSavingRef = true
alt First save — no existing conversation
UI->>API: POST /ai-conversations
API->>DB: INSERT AiConversation
API->>DB: INSERT AiMessages (separate op, no tx)
DB-->>API: ok
API-->>UI: { id, title }
Note over UI: isSavingRef = false
else Subsequent save — conversation exists
UI->>API: PUT /ai-conversations/:id/messages
API->>DB: DELETE AiMessages WHERE conversationId=:id
Note over DB: ⚠️ No transaction — if next step fails, messages gone
API->>DB: INSERT AiMessages (createMany)
DB-->>API: ok
API-->>UI: 200 {}
Note over UI: isSavingRef = false
end
Note over UI: ⚠️ If 2nd response completes while isSavingRef=true,
Note over UI: that save is silently skipped
Reviews (1): Last reviewed commit: "initial commit" | Re-trigger Greptile |
| } | ||
| }); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, []); | ||
|
|
There was a problem hiding this comment.
Suppressed exhaustive-deps hides a stale-closure bug
The eslint-disable-next-line react-hooks/exhaustive-deps suppresses warnings for currentUser and projectId missing from the dependency array. If either changes while the companion is open, the widget will continue showing the old project's conversations without reloading.
There was a problem hiding this comment.
Pull request overview
Adds persistent “Ask AI” chat history to the Dashboard Stack Companion by introducing conversation storage in the backend, a new chat-history UI in the companion drawer, and shared rendering utilities for AI chat messages.
Changes:
- Introduces new backend persistence models (
AiConversation,AiMessage) with migration and internal CRUD-like endpoints under/api/latest/internal/ai-conversations. - Adds a new Stack Companion “Ask AI” sidebar item and a full chat widget with conversation list, create/update/delete flows.
- Refactors shared AI chat rendering helpers into
ai-chat-shared.tsxand wires them into the existingask-aicommand UI.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Lockfile updates from dependency graph changes. |
| apps/dashboard/src/hooks/use-ai-conversations.ts | Client-side API wrapper for listing/creating/updating/deleting persisted conversations. |
| apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx | New Ask AI chat UI with conversation list + persistence integration. |
| apps/dashboard/src/components/stack-companion.tsx | Adds “Ask AI” entry and renders the new widget in the companion drawer. |
| apps/dashboard/src/components/design-components/list.tsx | Stops click propagation on list-item button container to avoid triggering row click. |
| apps/dashboard/src/components/commands/ask-ai.tsx | Uses shared AI chat UI helpers after refactor. |
| apps/dashboard/src/components/commands/ai-chat-shared.tsx | New shared components/utilities for AI chat message rendering and streaming effects. |
| apps/backend/src/app/api/latest/internal/ai-conversations/route.tsx | Adds list/create endpoints for conversations. |
| apps/backend/src/app/api/latest/internal/ai-conversations/[conversationId]/route.tsx | Adds get/update-title/delete endpoints for a specific conversation. |
| apps/backend/src/app/api/latest/internal/ai-conversations/[conversationId]/messages/route.tsx | Adds “replace all messages” endpoint for a conversation. |
| apps/backend/prisma/schema.prisma | Adds Prisma models for persistent AI chat storage. |
| apps/backend/prisma/migrations/20260327000000_add_ai_conversations/migration.sql | Creates DB tables/indexes/foreign key for AI chat persistence. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <button | ||
| onClick={handleSubmit} | ||
| disabled={!input.trim() || aiLoading} | ||
| className={cn( | ||
| "p-1 rounded transition-colors hover:transition-none", |
There was a problem hiding this comment.
Icon-only buttons should have an accessible name for screen readers. Add an aria-label (and/or title) to this send button (and similar icon-only buttons in the widget) so it’s discoverable to assistive tech.
| if ( | ||
| (completedOk || completedWithError) && | ||
| messages.length > 0 && | ||
| !isSavingRef.current | ||
| ) { |
There was a problem hiding this comment.
The isSavingRef guard can cause message history to be dropped: after a stream completes, saving happens while status === "ready" (so the input is enabled). If the user sends another message before the save finishes, the next completion will be ignored because isSavingRef.current is still true. Consider queuing a pending save (always save the latest messages after the current save finishes) or using a debounced/serial save loop keyed by conversationId.
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (4)
apps/dashboard/src/components/commands/ai-chat-shared.tsx (2)
188-190: Type assertions on external AI SDK data.The
invocation.inputandinvocation.outputare cast to specific shapes without validation. If the AI SDK response structure changes, this could cause silent failures or crashes when accessinginput?.queryorresult?.success.Consider adding defensive checks or optional chaining throughout the usage sites.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx` around lines 188 - 190, The code currently force-casts invocation.input and invocation.output to specific shapes (see invocation, input, queryArg, result) which is unsafe; update usages to validate types before access by checking that invocation?.input is an object and invocation?.output is an object with expected keys (e.g., typeof input.query === "string" before assigning queryArg and checking typeof result.success === "boolean" or Array.isArray(result.result) before using them), replace direct assumptions with optional chaining and safe guards, and add a small fallback/default values when fields are missing so consumer code using queryArg, result.success, result.result, and result.rowCount won’t throw if the SDK shape changes.
18-22: Potential state update on unmounted component.The
setTimeoutcallback could fire after the component unmounts, causing a React warning. While minor for visual state, it's good practice to clean up timeouts.Proposed fix using ref
export const CopyButton = memo(function CopyButton({ text, className, size = "sm" }: { text: string, className?: string, size?: "sm" | "xs", }) { const [copied, setCopied] = useState(false); + const timeoutRef = useRef<ReturnType<typeof setTimeout>>(); + + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); const handleCopy = useCallback(async () => { await navigator.clipboard.writeText(text); setCopied(true); - setTimeout(() => setCopied(false), 1500); + timeoutRef.current = setTimeout(() => setCopied(false), 1500); }, [text]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx` around lines 18 - 22, The handleCopy handler schedules a setTimeout to reset copied state which may run after the component unmounts; modify handleCopy and the component to store the timeout id in a ref (e.g., timeoutRef) and clear it in a useEffect cleanup to avoid calling setCopied on an unmounted component: when calling setTimeout in handleCopy save the returned id to timeoutRef.current, and in a useEffect return a cleanup that calls clearTimeout(timeoutRef.current) and resets timeoutRef.current to null; also clear any existing timeout before setting a new one in handleCopy to prevent overlaps.apps/dashboard/src/hooks/use-ai-conversations.ts (1)
41-43: Error message lacks response body context.The error message only includes the status code. For debugging, it would help to include the response body (if available) to understand why the request failed.
Proposed enhancement
if (!response.ok) { - throw new Error(`AI conversations API error: ${response.status}`); + const errorBody = await response.text().catch(() => ""); + throw new Error(`AI conversations API error: ${response.status}${errorBody ? ` - ${errorBody}` : ""}`); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/hooks/use-ai-conversations.ts` around lines 41 - 43, The error thrown when response.ok is false in use-ai-conversations.ts only includes the status code; update the failure path in the function that performs the fetch (e.g., the fetch call inside useAIConversations or fetchAIConversations) to read the response body (preferably via response.text() or response.json() guarded in a try/catch), include a truncated/limited portion of that body in the error message along with the status (e.g., `AI conversations API error: ${response.status} - ${bodySnippet}`), and fall back to a safe message if reading the body fails to avoid masking the original error.apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx (1)
564-568: Complex last-assistant detection logic could be simplified.The calculation for
isLastAssistantis difficult to follow. Consider extracting to a named helper or adding a brief comment explaining the intent.Suggested clarification
+ // Check if this is the last assistant message (accounting for trailing user message) const isLastAssistant = message.role === "assistant" && index === arr.length - 1 - (arr[arr.length - 1]?.role === "user" ? 1 : 0);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx` around lines 564 - 568, The last-assistant detection logic (currently computed in the isLastAssistant const using message.role, index, arr and the end-user check) is hard to read—extract this into a small helper function (e.g., isLastAssistantAtIndex(messages, index) or computeIsLastAssistant) and use that helper in place of the inline expression; inside the helper add a one-line comment explaining the rule (treat the final assistant as not-last if the very last message is a user), then update the displayContent line to use the new helper while keeping existing dependencies like messageContent and aiLoading unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/backend/prisma/migrations/20260327000000_add_ai_conversations/migration.sql`:
- Around line 2-11: The AiConversation table is missing a foreign-key constraint
on projectId which allows orphaned conversations; update the migration SQL to
add a foreign key constraint on "projectId" referencing the primary key of the
Project table (e.g., "Project"."id") with ON DELETE CASCADE, and then update the
Prisma schema to add the corresponding relation fields (e.g., in the
AiConversation model add an explicit relation to Project and mark projectId as
the foreign key) so Prisma migrations and runtime enforce cascade deletes; apply
the same change for the other similar table(s) referenced (lines 30-31) to keep
behavior consistent.
In `@apps/backend/prisma/schema.prisma`:
- Around line 1105-1113: The AiMessage model currently relies on createdAt for
ordering which is nondeterministic for batch inserts (createMany()); add an
explicit integer position field to AiMessage (e.g., position Int) and create a
composite index on (conversationId, position) to preserve stable ordering, then
update any insertion code that uses createMany() or individual message inserts
to set position according to the input array order (ensuring position increments
per conversation) and change queries that used orderBy: { createdAt: "asc" } to
orderBy: { position: "asc" } (keep conversationId and createdAt untouched aside
from indexing).
In
`@apps/backend/src/app/api/latest/internal/ai-conversations/`[conversationId]/messages/route.tsx:
- Around line 46-61: The current handler calls getOwnedConversation then deletes
messages and separately calls createMany, which can leave the conversation empty
on failure and doesn't update AiConversation.updatedAt; wrap the deleteMany +
optional createMany + an update to touch the parent conversation (update
AiConversation.updatedAt for params.conversationId) inside a single Prisma
transaction (use globalPrismaClient.$transaction) so the delete/insert/update
are atomic and the parent conversation's updatedAt is updated even when messages
array is empty; keep getOwnedConversation as the authorization check before the
transaction and reference params.conversationId,
globalPrismaClient.aiMessage.deleteMany,
globalPrismaClient.aiMessage.createMany, and
globalPrismaClient.aiConversation.update (or the appropriate model name) when
implementing.
In `@apps/backend/src/app/api/latest/internal/ai-conversations/route.tsx`:
- Around line 97-114: The code creates an aiConversation and then calls
globalPrismaClient.aiMessage.createMany separately, which can leave a dangling
conversation if message insertion fails; change this to an atomic operation by
either wrapping both calls in a Prisma transaction (use
globalPrismaClient.$transaction([ ... ]) to create the conversation and create
messages) or perform a nested create in the aiConversation.create call (include
the relation field and use messages: { create: body.messages.map(m => ({ role:
m.role, content: m.content })) } so messages are created as part of the same
write). Update the handler to use one of these approaches instead of separate
aiConversation.create and aiMessage.createMany calls.
- Around line 77-85: The request body schema's messages array uses yupString()
for role and yupMixed() for content, causing downstream casts (e.g., the
"msg.content as object" usage); update the body: yupObject(...) messages schema
so role is constrained with
yupString().oneOf(["user","assistant","system"]).defined() and content is
validated as the exact persisted message object shape (replace yupMixed() with a
yupObject(...) that matches the stored message fields), and apply the identical
tightened schema to the sibling PUT handler in
[conversationId]/messages/route.tsx so callers and handlers no longer need
runtime type casts.
In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx`:
- Around line 408-412: The getToolInvocations function uses a double cast (as
unknown as ToolInvocationPart) which bypasses TypeScript safety; replace this
with a proper type guard or runtime validation: add a predicate like
isToolInvocationPart(part): part is ToolInvocationPart (checking required
properties/shape of ToolInvocationPart) and use message.parts.filter((part):
part is ToolInvocationPart => part.type.startsWith("tool-") &&
isToolInvocationPart(part)) then return the filtered array directly; reference
getToolInvocations, UIMessage, and ToolInvocationPart when adding the guard.
In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx`:
- Around line 185-201: The effect that fetches the latest conversation (inside
useEffect) omits projectId from its dependency array, causing stale
conversations when the user switches projects; update the dependency list to
include projectId (and any other used values like currentUser or
runAsynchronously if not stable) so the effect re-runs on project change,
ensuring listConversations/currentUser/getConversation, setViewMode, and
setConversationKey operate against the correct project; alternatively ensure the
component remounts on project change, but the simplest fix is to add projectId
to the useEffect dependencies.
- Around line 352-368: The save block using runAsynchronously currently lets
errors from createConversation or replaceConversationMessages fail silently;
update the inner async to catch errors (surround the await calls for
replaceConversationMessages/createConversation with try/catch), call a
user-visible notifier (e.g., runAsynchronouslyWithAlert or the app's toast/alert
mechanism) inside the catch with a clear message including the error, still set
isSavingRef.current = false in finally, and keep the existing logic that sets
conversationIdRef.current and calls onConversationCreated(result.id) only on
success.
- Around line 191-195: The code in initialMessages maps conv.messages using
unsafe casts for msg.role and msg.content; update the
ConversationDetail.messages type to reflect the real API shape (role: "user" |
"assistant" and content: UIMessage["parts"]) or add runtime validation before
mapping (validate msg.role is "user"|"assistant" and that msg.content conforms
to UIMessage["parts"]) and only then construct initialMessages from
conv.messages to avoid unsafe casts in the initialMessages mapping.
In `@apps/dashboard/src/hooks/use-ai-conversations.ts`:
- Around line 67-73: The code interpolates conversationId directly into request
paths (see getConversation, updateConversationTitle,
replaceConversationMessages, deleteConversation), which risks path traversal if
an attacker supplies malicious input; fix by validating or encoding the id
before using it in the URL: either enforce a strict format (e.g., UUID regex)
and throw on invalid values, or always URL-encode conversationId with
encodeURIComponent() and use the encoded value in apiFetch; apply the same
validation/encoding pattern consistently across the listed functions.
---
Nitpick comments:
In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx`:
- Around line 188-190: The code currently force-casts invocation.input and
invocation.output to specific shapes (see invocation, input, queryArg, result)
which is unsafe; update usages to validate types before access by checking that
invocation?.input is an object and invocation?.output is an object with expected
keys (e.g., typeof input.query === "string" before assigning queryArg and
checking typeof result.success === "boolean" or Array.isArray(result.result)
before using them), replace direct assumptions with optional chaining and safe
guards, and add a small fallback/default values when fields are missing so
consumer code using queryArg, result.success, result.result, and result.rowCount
won’t throw if the SDK shape changes.
- Around line 18-22: The handleCopy handler schedules a setTimeout to reset
copied state which may run after the component unmounts; modify handleCopy and
the component to store the timeout id in a ref (e.g., timeoutRef) and clear it
in a useEffect cleanup to avoid calling setCopied on an unmounted component:
when calling setTimeout in handleCopy save the returned id to
timeoutRef.current, and in a useEffect return a cleanup that calls
clearTimeout(timeoutRef.current) and resets timeoutRef.current to null; also
clear any existing timeout before setting a new one in handleCopy to prevent
overlaps.
In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx`:
- Around line 564-568: The last-assistant detection logic (currently computed in
the isLastAssistant const using message.role, index, arr and the end-user check)
is hard to read—extract this into a small helper function (e.g.,
isLastAssistantAtIndex(messages, index) or computeIsLastAssistant) and use that
helper in place of the inline expression; inside the helper add a one-line
comment explaining the rule (treat the final assistant as not-last if the very
last message is a user), then update the displayContent line to use the new
helper while keeping existing dependencies like messageContent and aiLoading
unchanged.
In `@apps/dashboard/src/hooks/use-ai-conversations.ts`:
- Around line 41-43: The error thrown when response.ok is false in
use-ai-conversations.ts only includes the status code; update the failure path
in the function that performs the fetch (e.g., the fetch call inside
useAIConversations or fetchAIConversations) to read the response body
(preferably via response.text() or response.json() guarded in a try/catch),
include a truncated/limited portion of that body in the error message along with
the status (e.g., `AI conversations API error: ${response.status} -
${bodySnippet}`), and fall back to a safe message if reading the body fails to
avoid masking the original error.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9b7586bc-5742-4613-b78e-b4381f9af0ef
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
apps/backend/prisma/migrations/20260327000000_add_ai_conversations/migration.sqlapps/backend/prisma/schema.prismaapps/backend/src/app/api/latest/internal/ai-conversations/[conversationId]/messages/route.tsxapps/backend/src/app/api/latest/internal/ai-conversations/[conversationId]/route.tsxapps/backend/src/app/api/latest/internal/ai-conversations/route.tsxapps/dashboard/src/components/commands/ai-chat-shared.tsxapps/dashboard/src/components/commands/ask-ai.tsxapps/dashboard/src/components/design-components/list.tsxapps/dashboard/src/components/stack-companion.tsxapps/dashboard/src/components/stack-companion/ai-chat-widget.tsxapps/dashboard/src/hooks/use-ai-conversations.ts
| body: yupObject({ | ||
| title: yupString().defined(), | ||
| projectId: yupString().defined(), | ||
| messages: yupArray( | ||
| yupObject({ | ||
| role: yupString().defined(), | ||
| content: yupMixed().defined(), | ||
| }) | ||
| ).defined(), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /tmp && find . -type f -name "route.tsx" | grep -i "ai-conversations" | head -5Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
find . -type f -path "*internal/ai-conversations*" -name "*.tsx"Repository: stack-auth/stack-auth
Length of output: 317
🏁 Script executed:
find . -type f -path "*ai-conversations*" -name "*.ts*" | head -10Repository: stack-auth/stack-auth
Length of output: 368
🏁 Script executed:
fd -t f "route.tsx" apps/backend/src/app/api/latest/internal --max-one-result | head -5Repository: stack-auth/stack-auth
Length of output: 298
🏁 Script executed:
fd -t d "ai-conversations" apps/backend/src/Repository: stack-auth/stack-auth
Length of output: 123
🏁 Script executed:
fd -t d "internal" apps/backend/src/app/api/latest/Repository: stack-auth/stack-auth
Length of output: 228
🏁 Script executed:
git ls-files "apps/backend/src/app/api/latest/internal/"Repository: stack-auth/stack-auth
Length of output: 4831
🏁 Script executed:
cat -n apps/backend/src/app/api/latest/internal/ai-conversations/route.tsx | sed -n '70,115p'Repository: stack-auth/stack-auth
Length of output: 1670
🏁 Script executed:
wc -l apps/backend/src/app/api/latest/internal/ai-conversations/route.tsxRepository: stack-auth/stack-auth
Length of output: 136
🏁 Script executed:
cat -n apps/backend/src/app/api/latest/internal/ai-conversations/route.tsxRepository: stack-auth/stack-auth
Length of output: 4414
🏁 Script executed:
cat -n apps/backend/src/app/api/latest/internal/ai-conversations/[conversationId]/messages/route.tsxRepository: stack-auth/stack-auth
Length of output: 2706
🏁 Script executed:
wc -l apps/backend/src/app/api/latest/internal/ai-conversations/[conversationId]/messages/route.tsxRepository: stack-auth/stack-auth
Length of output: 161
🏁 Script executed:
fd -t f "schema.prisma" apps/backendRepository: stack-auth/stack-auth
Length of output: 98
🏁 Script executed:
grep -A 5 -B 5 "aiMessage\|AiMessage" $(fd -t f "schema.prisma" apps/backend)Repository: stack-auth/stack-auth
Length of output: 518
🏁 Script executed:
find . -path "*/prisma/schema.prisma" -o -path "*/schema.prisma" | head -3Repository: stack-auth/stack-auth
Length of output: 100
🏁 Script executed:
rg -A 5 -B 5 "yupMixed" apps/backend/src/app/api/latest/internal/ai-conversations/Repository: stack-auth/stack-auth
Length of output: 7205
🏁 Script executed:
grep -r "yupMixed" packages/stack-shared/dist/schema-fields.ts | head -5Repository: stack-auth/stack-auth
Length of output: 141
🏁 Script executed:
cat $(find packages/stack-shared -name "*schema-fields*" -o -name "*schema*" | grep -i field | head -1)Repository: stack-auth/stack-auth
Length of output: 50377
🏁 Script executed:
fd -t f "schema-fields" packages/stack-shared/Repository: stack-auth/stack-auth
Length of output: 107
🏁 Script executed:
grep -A 20 "model AiMessage" apps/backend/prisma/schema.prismaRepository: stack-auth/stack-auth
Length of output: 541
🏁 Script executed:
rg -B 5 -A 15 "AiMessage|aiMessage" apps/backend/prisma/schema.prismaRepository: stack-auth/stack-auth
Length of output: 722
🏁 Script executed:
rg -r "AiMessage\|aiMessage" --type ts --type tsx apps/backend/src | grep -i "content\|schema" | head -20Repository: stack-auth/stack-auth
Length of output: 92
🏁 Script executed:
rg "messages\[" apps/backend/src/app/api/latest/internal/ai-conversations/ -A 3 -B 3Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
rg "aiMessage\|AiMessage" apps/backend/src --type ts --type js | head -20Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
find apps/backend/src -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "aiMessage" | head -5Repository: stack-auth/stack-auth
Length of output: 311
🏁 Script executed:
rg "role.*user|assistant|system" apps/backend/src/app/api/latest/internal/ai-conversations/ -iRepository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
grep -n "as object\|as any\|as unknown" apps/backend/src/app/api/latest/internal/ai-conversations/route.tsxRepository: stack-auth/stack-auth
Length of output: 110
🏁 Script executed:
grep -n "as object\|as any\|as unknown" apps/backend/src/app/api/latest/internal/ai-conversations/[conversationId]/messages/route.tsxRepository: stack-auth/stack-auth
Length of output: 109
Tighten the request message schema to define expected role and content types.
The messages[*] schema accepts arbitrary yupMixed() for content and unvalidated strings for role, which requires downstream type casts (msg.content as object at lines 111 and 58). This violates the coding guidelines: "Do NOT use as/any/type casts or anything else to bypass the type system unless you specifically asked the user about it."
Define the exact persisted message structure here—for example, role should be constrained to known values (e.g., "user", "assistant", "system") and content should be validated as a specific type. Apply the same schema to the sibling PUT route at [conversationId]/messages/route.tsx to avoid duplication.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/backend/src/app/api/latest/internal/ai-conversations/route.tsx` around
lines 77 - 85, The request body schema's messages array uses yupString() for
role and yupMixed() for content, causing downstream casts (e.g., the
"msg.content as object" usage); update the body: yupObject(...) messages schema
so role is constrained with
yupString().oneOf(["user","assistant","system"]).defined() and content is
validated as the exact persisted message object shape (replace yupMixed() with a
yupObject(...) that matches the stored message fields), and apply the identical
tightened schema to the sibling PUT handler in
[conversationId]/messages/route.tsx so callers and handlers no longer need
runtime type casts.
There was a problem hiding this comment.
role is already validated. content stores the ai sdk's message parts which can have many different shapes (text, tool calls, tool results etc)
There was a problem hiding this comment.
@aadesh18, understood — thanks for the clarification! Since content holds AI SDK message parts (text, tool calls, tool results, etc.) with varying shapes, yupMixed() is the right choice there, and I can see role is already validated as a string. I'll note this for future reviews.
✏️ Learnings added
Learnt from: aadesh18
Repo: stack-auth/stack-auth PR: 1296
File: apps/backend/src/app/api/latest/internal/ai-conversations/route.tsx:77-85
Timestamp: 2026-03-30T19:28:58.193Z
Learning: In `apps/backend/src/app/api/latest/internal/ai-conversations/route.tsx` and `[conversationId]/messages/route.tsx`, the `content` field in the messages array intentionally uses `yupMixed()` because it stores AI SDK message parts (text parts, tool call parts, tool result parts, etc.) which are polymorphic and have many different shapes. The `as object` cast for content is therefore acceptable in this context. Do not flag this as a type-safety violation.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-18T00:20:36.985Z
Learning: Applies to apps/backend/src/app/api/**/*.ts : The API follows a RESTful design with routes organized by resource type (auth, users, teams, oauth-providers)
Learnt from: nams1570
Repo: stack-auth/stack-auth PR: 1221
File: packages/stack-shared/src/interface/crud/email-outbox.ts:44-47
Timestamp: 2026-02-25T23:48:41.843Z
Learning: In packages/stack-shared/src/interface/crud/email-outbox.ts, the email outbox read schema does not enforce discriminated union constraints between created_with, email_draft_id, and email_programmatic_call_template_id at the Yup schema level. This is intentional because: (1) the schema is read-only (data comes from DB, not user input), (2) DB-level CHECK constraints (EmailOutbox_email_draft_check, EmailOutbox_email_draft_reverse_check, EmailOutbox_email_programmatic_call_template_check) already enforce these invariants as the source of truth, and (3) adding complex discriminated union validation to read schemas would add unnecessary complexity without benefit.
Learnt from: nams1570
Repo: stack-auth/stack-auth PR: 1191
File: apps/backend/src/app/api/latest/emails/outbox/crud.tsx:61-78
Timestamp: 2026-02-13T18:56:51.660Z
Learning: In apps/backend/src/app/api/latest/emails/outbox/crud.tsx, the prismaModelToCrud function uses type casts (including for sendAttemptErrors) because EmailOutboxCrud["Server"]["Read"] is a complex discriminated union that TypeScript cannot properly infer through object spread patterns. This is documented in the inline comment at lines 49-52 and is an accepted pattern in this file.
Learnt from: nams1570
Repo: stack-auth/stack-auth PR: 1214
File: apps/backend/src/lib/stripe.tsx:87-93
Timestamp: 2026-02-19T22:05:01.961Z
Learning: Do not perform runtime validation when Prisma JsonValue fields are cast to concrete schema types in this codebase if the data originates from controlled sources (e.g., seeded products) and has been validated before insertion. This pattern is used in multiple locations (payments.tsx, transaction-builder.ts, switch/route.ts, refund/route.tsx, stripe.tsx). If you cannot guarantee source validity, apply runtime validation before use. Maintain consistency by documenting the trusted-data assumption where these casts occur and avoid introducing new unsafe casts without justification.
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 1239
File: apps/backend/src/lib/tokens.tsx:49-50
Timestamp: 2026-03-16T18:40:49.743Z
Learning: Adopt the convention // TODO next-release in this codebase for temporary backward-compat shims that only need to survive one release. When flagging backward-incompatible changes with a short transition window (e.g., turning a required field optional), mark the change with a // TODO next-release and rely on removal in the next release, instead of implementing a complex versioning scheme. This pattern is exemplified in apps/backend/src/lib/tokens.tsx and should be used consistently for similar files in the backend.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…to ai-chat-history
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes