Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/sim/app/api/mothership/chats/[chatId]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ vi.mock('drizzle-orm', () => ({
vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)

vi.mock('@/lib/copilot/chat/lifecycle', () => ({
getAccessibleCopilotChat: mockGetAccessibleCopilotChat,
getAccessibleCopilotChatAuth: mockGetAccessibleCopilotChat,
getAccessibleCopilotChatWithMessages: mockGetAccessibleCopilotChat,
}))

vi.mock('@/lib/copilot/chat/stream-liveness', () => ({
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/mothership/chats/[chatId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { parseRequest } from '@/lib/api/server'
import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository'
import { buildEffectiveChatTranscript } from '@/lib/copilot/chat/effective-transcript'
import {
getAccessibleCopilotChat,
getAccessibleCopilotChatAuth,
getAccessibleCopilotChatWithMessages,
} from '@/lib/copilot/chat/lifecycle'
import { normalizeMessage } from '@/lib/copilot/chat/persisted-message'
import { reconcileChatStreamMarkers } from '@/lib/copilot/chat/stream-liveness'
Expand Down Expand Up @@ -45,7 +45,7 @@ export const GET = withRouteHandler(
if (!paramsResult.success) return paramsResult.response
const { chatId } = paramsResult.data.params

const chat = await getAccessibleCopilotChat(chatId, userId)
const chat = await getAccessibleCopilotChatWithMessages(chatId, userId)
if (!chat || chat.type !== 'mothership') {
return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 })
}
Expand Down
65 changes: 60 additions & 5 deletions apps/sim/lib/copilot/chat/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const logger = createLogger('CopilotChatLifecycle')

export interface ChatLoadResult {
chatId: string
chat: typeof copilotChats.$inferSelect | null
chat: CopilotChatDetailRow | null
conversationHistory: unknown[]
isNew: boolean
}
Expand All @@ -34,11 +34,43 @@ const copilotChatAuthColumns = {
type: copilotChats.type,
} as const

/**
* Column set for chat-detail callers that need the conversation transcript but
* not the copilot-only TOAST-able fields (`previewYaml`, `planArtifact`,
* `config`) or unused metadata (`model`, `pinned`, `lastSeenAt`). Selecting
* only these columns avoids the Postgres detoast cost on the dropped fields,
* which dominates latency for chats with large message histories.
*/
const copilotChatDetailColumns = {
...copilotChatAuthColumns,
title: copilotChats.title,
messages: copilotChats.messages,
conversationId: copilotChats.conversationId,
resources: copilotChats.resources,
createdAt: copilotChats.createdAt,
updatedAt: copilotChats.updatedAt,
} as const

type CopilotChatAuthRow = Pick<
typeof copilotChats.$inferSelect,
'id' | 'userId' | 'workflowId' | 'workspaceId' | 'type'
>

export type CopilotChatDetailRow = Pick<
typeof copilotChats.$inferSelect,
| 'id'
| 'userId'
| 'workflowId'
| 'workspaceId'
| 'type'
| 'title'
| 'messages'
| 'conversationId'
| 'resources'
| 'createdAt'
| 'updatedAt'
>

async function authorizeCopilotChatRow<T extends CopilotChatAuthRow>(
chat: T | undefined,
chatId: string,
Expand Down Expand Up @@ -99,8 +131,10 @@ export async function getAccessibleCopilotChatAuth(

/**
* Load the full copilot chat row after authorization. Use this only when the
* caller actually consumes the heavy columns (`messages`, `planArtifact`,
* `config`, etc.) — for example, chat resume or the GET-by-id endpoint.
* caller actually consumes copilot-only TOAST-able columns (`previewYaml`,
* `planArtifact`, `config`) or other extended metadata — for example the
* legacy copilot chat detail endpoint. Mothership chats and other consumers
* that only need the transcript should prefer `getAccessibleCopilotChatWithMessages`.
*/
export async function getAccessibleCopilotChat(chatId: string, userId: string) {
const [chat] = await db
Expand All @@ -112,6 +146,27 @@ export async function getAccessibleCopilotChat(chatId: string, userId: string) {
return authorizeCopilotChatRow(chat, chatId, userId)
}

/**
* Load a copilot chat with the conversation transcript and resources after
* authorization, omitting copilot-only TOAST-able fields (`previewYaml`,
* `planArtifact`, `config`) and unused metadata (`model`, `pinned`,
* `lastSeenAt`). Use this for the mothership chat detail endpoint and the
* shared `resolveOrCreateChat` path — every column read here is consumed
* downstream, and dropping the others avoids per-request detoast overhead.
*/
export async function getAccessibleCopilotChatWithMessages(
chatId: string,
userId: string
): Promise<CopilotChatDetailRow | null> {
const [chat] = await db
.select(copilotChatDetailColumns)
.from(copilotChats)
.where(and(eq(copilotChats.id, chatId), eq(copilotChats.userId, userId)))
.limit(1)

return authorizeCopilotChatRow(chat, chatId, userId)
}

/**
* Resolve or create a copilot chat session.
* If chatId is provided, loads the existing chat. Otherwise creates a new one.
Expand All @@ -132,7 +187,7 @@ export async function resolveOrCreateChat(params: {
}

if (chatId) {
const chat = await getAccessibleCopilotChat(chatId, userId)
const chat = await getAccessibleCopilotChatWithMessages(chatId, userId)

if (chat) {
if (workflowId && chat.workflowId !== workflowId) {
Expand Down Expand Up @@ -189,7 +244,7 @@ export async function resolveOrCreateChat(params: {
messages: [],
lastSeenAt: now,
})
.returning()
.returning(copilotChatDetailColumns)

if (!newChat) {
logger.warn('Failed to create new copilot chat row', { userId, workflowId, workspaceId })
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/copilot/chat/process-contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ async function processPastChatFromDb(
currentWorkspaceId?: string
): Promise<AgentContext | null> {
try {
const { getAccessibleCopilotChat } = await import('./lifecycle')
const chat = await getAccessibleCopilotChat(chatId, userId)
const { getAccessibleCopilotChatWithMessages } = await import('./lifecycle')
const chat = await getAccessibleCopilotChatWithMessages(chatId, userId)
if (!chat) {
return null
}
Expand Down
Loading