Skip to content

Commit 5eb0b33

Browse files
authored
fix(copilot): load workflow chat history through the windowed chat routes (#9113)
* fix(copilot): load workflow chat history through the windowed chat routes * fix(copilot): scope workflow chats without a workspace through their workflow * test(copilot): name each workflow chat fixture's workflow uniquely
1 parent 30fc8e3 commit 5eb0b33

18 files changed

Lines changed: 247 additions & 367 deletions

‎apps/sim/app/api/copilot/chat/queries.ts‎

Lines changed: 0 additions & 183 deletions
This file was deleted.

‎apps/sim/app/api/mothership/chat/route.ts‎

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { type NextRequest, NextResponse } from 'next/server'
2-
import {
3-
mothershipChatGetQuerySchema,
4-
mothershipChatPostEnvelopeSchema,
5-
} from '@/lib/api/contracts/mothership-chats'
6-
import { validationErrorResponse } from '@/lib/api/server'
2+
import { mothershipChatPostEnvelopeSchema } from '@/lib/api/contracts/mothership-chats'
73
import { getSession } from '@/lib/auth'
84
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
95
import { handleUnifiedChatPost } from '@/lib/mothership/chat/post'
106
import { validateShimEnvelope } from '@/lib/mothership/request/http'
11-
import { GET as copilotChatGet } from '@/app/api/copilot/chat/queries'
127

138
/**
149
* Caps this response on serverless hosts only; the Node server bounds nothing with it.
@@ -17,16 +12,6 @@ import { GET as copilotChatGet } from '@/app/api/copilot/chat/queries'
1712
*/
1813
export const maxDuration = 3600
1914

20-
// Unified chat route surface.
21-
export const GET = withRouteHandler((request: NextRequest) => {
22-
const validation = mothershipChatGetQuerySchema.safeParse(
23-
Object.fromEntries(request.nextUrl.searchParams.entries())
24-
)
25-
if (!validation.success) return validationErrorResponse(validation.error)
26-
27-
return copilotChatGet(request)
28-
})
29-
3015
export const POST = withRouteHandler(async (request: NextRequest) => {
3116
const session = await getSession()
3217
if (!session?.user?.id) {

‎apps/sim/app/api/mothership/chats/[chatId]/route.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export const GET = withRouteHandler(
5353

5454
const chat = await getAccessibleCopilotChatWithMessages(chatId, userId, {
5555
principal,
56-
type: 'mothership',
5756
window: fromPosition
5857
? { kind: 'from', position: fromPosition }
5958
: window === 'latest'

‎apps/sim/hooks/queries/mothership-chats.ts‎

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -370,29 +370,12 @@ async function readMothershipChatHistory(
370370
windowStartCursor: string | null | undefined,
371371
signal?: AbortSignal
372372
): Promise<MothershipChatHistory> {
373-
try {
374-
const data = await requestJson(getMothershipChatContract, {
375-
params: { chatId },
376-
query: windowStartCursor ? { from: windowStartCursor } : { window: 'latest' },
377-
signal,
378-
})
379-
return parseChatHistory(data)
380-
} catch (error) {
381-
if (!isApiClientError(error) || error.status !== 404) throw error
382-
}
383-
384-
// boundary-raw-fetch: legacy alias path /api/mothership/chat?chatId=... returns the
385-
// copilot lifecycle shape (activeStreamId, not conversationId) for chats stored under
386-
// the older copilot table; no contract exists for this alias path
387-
const copilotRes = await fetch(`/api/mothership/chat?chatId=${encodeURIComponent(chatId)}`, {
373+
const data = await requestJson(getMothershipChatContract, {
374+
params: { chatId },
375+
query: windowStartCursor ? { from: windowStartCursor } : { window: 'latest' },
388376
signal,
389377
})
390-
391-
if (!copilotRes.ok) {
392-
throw new Error('Failed to load chat')
393-
}
394-
395-
return parseChatHistory(await copilotRes.json())
378+
return parseChatHistory(data)
396379
}
397380

398381
/**

‎apps/sim/lib/api/contracts/copilot.ts‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,6 @@ export const copilotToolExecuteInternalBodySchema = z
186186
)
187187
export type CopilotToolExecuteInternalBody = z.input<typeof copilotToolExecuteInternalBodySchema>
188188

189-
export const copilotChatGetQuerySchema = z
190-
.object({
191-
workflowId: z.string().optional(),
192-
workspaceId: z.string().optional(),
193-
chatId: z.string().optional(),
194-
})
195-
.passthrough()
196-
197189
export const copilotChatStreamQuerySchema = z.object({
198190
streamId: z.string().optional().default(''),
199191
after: z.string().optional().default(''),

‎apps/sim/lib/api/contracts/mothership-chats.ts‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,15 +168,6 @@ export type MothershipExecuteBody = z.input<typeof mothershipExecuteBodySchema>
168168

169169
export const mothershipEventsQuerySchema = mothershipChatOwnerSchema
170170

171-
export const mothershipChatGetQuerySchema = z
172-
.object({
173-
workflowId: z.string().optional(),
174-
workspaceId: z.string().optional(),
175-
organizationId: z.string().optional(),
176-
chatId: z.string().optional(),
177-
})
178-
.passthrough()
179-
180171
export const mothershipChatPostEnvelopeSchema = z
181172
.object({
182173
message: z.string().max(MAX_CHAT_MESSAGE_LENGTH).optional(),

‎apps/sim/lib/mothership/chat/application/context.ts‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getActivelyBannedUserIds } from '@/lib/auth/ban'
66
import type { WorkspaceAuthorizationContext } from '@/lib/core/application'
77
import { OrchestrationError } from '@/lib/core/orchestration/types'
88
import { conversationModeSelection } from '@/lib/mothership/chat/intent'
9+
import { authorizeChatWorkflowRead, isChatModeAvailable } from '@/lib/mothership/chat/lifecycle'
910
import { resolveActiveWorkspaceApplicationContext } from '@/lib/workspaces/application/context'
1011

1112
export type ChatOwnerContext =
@@ -37,6 +38,7 @@ export async function resolveOwnedChatContext(
3738
userId: copilotChats.userId,
3839
workspaceId: copilotChats.workspaceId,
3940
organizationId: copilotChats.organizationId,
41+
workflowId: copilotChats.workflowId,
4042
type: copilotChats.type,
4143
mode: conversationModeSelection,
4244
memorySpaceId: copilotChats.memorySpaceId,
@@ -46,7 +48,8 @@ export async function resolveOwnedChatContext(
4648
.limit(1)
4749
if (
4850
!chat ||
49-
Boolean(chat.workspaceId) === Boolean(chat.organizationId) ||
51+
(chat.workspaceId && chat.organizationId) ||
52+
(!chat.workspaceId && !chat.organizationId && !chat.workflowId) ||
5053
chat.userId !== resolvePrincipalSubjectUserId(principal) ||
5154
(chat.organizationId && chat.type !== 'mothership')
5255
) {
@@ -58,15 +61,49 @@ export async function resolveOwnedChatContext(
5861
const owner: ChatOwnerContext = chat.organizationId
5962
? { organizationId: chat.organizationId }
6063
: {
61-
...(await resolveActiveWorkspaceApplicationContext(chat.workspaceId!)),
64+
...(await resolveActiveWorkspaceApplicationContext(
65+
await resolveChatWorkspaceId(chatId, chat)
66+
)),
6267
organizationId: undefined,
6368
}
6469
return {
6570
...owner,
6671
chatId,
6772
userId: chat.userId,
73+
workflowId: chat.workflowId,
6874
type: chat.type,
6975
mode: chat.mode,
7076
memorySpaceId: chat.memorySpaceId ?? null,
7177
}
7278
}
79+
80+
/**
81+
* A workflow chat row from before chats carried a workspace is scoped by its workflow's, while
82+
* its owner may still read that workflow.
83+
*/
84+
async function resolveChatWorkspaceId(
85+
chatId: string,
86+
chat: { userId: string; workspaceId: string | null; workflowId: string | null }
87+
): Promise<string> {
88+
if (chat.workspaceId) return chat.workspaceId
89+
const workflow = chat.workflowId
90+
? await authorizeChatWorkflowRead(chatId, chat.userId, chat.workflowId)
91+
: null
92+
if (!workflow?.workspaceId) throw new OrchestrationError('not_found', 'Chat not found')
93+
return workflow.workspaceId
94+
}
95+
96+
/**
97+
* Refuses a chat its owner may no longer read, as the chat detail read does: a workflow chat
98+
* whose workflow they cannot read, or a workspace Plan chat while Plan mode is off for them.
99+
*/
100+
export async function requireOwnedChatReadable(
101+
context: Awaited<ReturnType<typeof resolveOwnedChatContext>>
102+
) {
103+
if (
104+
(context.workflowId &&
105+
!(await authorizeChatWorkflowRead(context.chatId, context.userId, context.workflowId))) ||
106+
!(await isChatModeAvailable(context, context.userId))
107+
)
108+
throw new OrchestrationError('not_found', 'Chat not found')
109+
}

‎apps/sim/lib/mothership/chat/application/read-older-messages.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { defineWorkspaceOperation } from '@/lib/core/application'
33
import { defineOrganizationOperation } from '@/lib/core/application/organization-operation'
44
import { OrchestrationError } from '@/lib/core/orchestration/types'
55
import { defineAuthorizedChatUseCase } from '@/lib/mothership/chat/application/authorized-chat-use-case'
6-
import { resolveOwnedChatContext } from '@/lib/mothership/chat/application/context'
6+
import {
7+
requireOwnedChatReadable,
8+
resolveOwnedChatContext,
9+
} from '@/lib/mothership/chat/application/context'
710
import { loadCopilotChatMessagesBefore } from '@/lib/mothership/chat/lifecycle'
811
import { compactToolCallParams, normalizeMessage } from '@/lib/mothership/chat/persisted-message'
912
import { decodeTranscriptCursor } from '@/lib/mothership/chat/transcript-cursor'
@@ -14,7 +17,7 @@ interface ReadChatOlderMessagesInput {
1417
}
1518

1619
/**
17-
* A page of the owner's mothership chat transcript before a cursor the chat detail read
20+
* A page of the owner's chat transcript before a cursor the chat detail read
1821
* issued, normalized and compacted exactly as that read returns its messages, so a message
1922
* the backfill has not reached still arrives without its bulk tool-call params.
2023
*/
@@ -47,7 +50,7 @@ export const readChatOlderMessages = defineAuthorizedChatUseCase({
4750
async execute({ context, input }) {
4851
const position = decodeTranscriptCursor(input.before)
4952
if (!position) throw new OrchestrationError('validation', 'Invalid cursor')
50-
if (context.type !== 'mothership') throw new OrchestrationError('not_found', 'Chat not found')
53+
await requireOwnedChatReadable(context)
5154
const page = await loadCopilotChatMessagesBefore(context.chatId, position)
5255
return {
5356
messages: page.messages.map((message) =>

0 commit comments

Comments
 (0)