improvement(copilot): drop unused columns from mothership chat detail reads (#4640)

This commit is contained in:
Waleed
2026-05-16 23:05:41 -07:00
committed by GitHub
parent 4945d55671
commit fd12137c28
4 changed files with 65 additions and 10 deletions
@@ -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', () => ({
@@ -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'
@@ -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 })
}
+60 -5
View File
@@ -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
}
@@ -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,
@@ -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
@@ -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.
@@ -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) {
@@ -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 })
@@ -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
}