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
183 changes: 0 additions & 183 deletions apps/sim/app/api/copilot/chat/queries.ts

This file was deleted.

17 changes: 1 addition & 16 deletions apps/sim/app/api/mothership/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { type NextRequest, NextResponse } from 'next/server'
import {
mothershipChatGetQuerySchema,
mothershipChatPostEnvelopeSchema,
} from '@/lib/api/contracts/mothership-chats'
import { validationErrorResponse } from '@/lib/api/server'
import { mothershipChatPostEnvelopeSchema } from '@/lib/api/contracts/mothership-chats'
import { getSession } from '@/lib/auth'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { handleUnifiedChatPost } from '@/lib/mothership/chat/post'
import { validateShimEnvelope } from '@/lib/mothership/request/http'
import { GET as copilotChatGet } from '@/app/api/copilot/chat/queries'

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

// Unified chat route surface.
export const GET = withRouteHandler((request: NextRequest) => {
const validation = mothershipChatGetQuerySchema.safeParse(
Object.fromEntries(request.nextUrl.searchParams.entries())
)
if (!validation.success) return validationErrorResponse(validation.error)

return copilotChatGet(request)
})

export const POST = withRouteHandler(async (request: NextRequest) => {
const session = await getSession()
if (!session?.user?.id) {
Expand Down
1 change: 0 additions & 1 deletion apps/sim/app/api/mothership/chats/[chatId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export const GET = withRouteHandler(

const chat = await getAccessibleCopilotChatWithMessages(chatId, userId, {
principal,
type: 'mothership',
window: fromPosition
? { kind: 'from', position: fromPosition }
: window === 'latest'
Expand Down
25 changes: 4 additions & 21 deletions apps/sim/hooks/queries/mothership-chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,29 +370,12 @@ async function readMothershipChatHistory(
windowStartCursor: string | null | undefined,
signal?: AbortSignal
): Promise<MothershipChatHistory> {
try {
const data = await requestJson(getMothershipChatContract, {
params: { chatId },
query: windowStartCursor ? { from: windowStartCursor } : { window: 'latest' },
signal,
})
return parseChatHistory(data)
} catch (error) {
if (!isApiClientError(error) || error.status !== 404) throw error
}

// boundary-raw-fetch: legacy alias path /api/mothership/chat?chatId=... returns the
// copilot lifecycle shape (activeStreamId, not conversationId) for chats stored under
// the older copilot table; no contract exists for this alias path
const copilotRes = await fetch(`/api/mothership/chat?chatId=${encodeURIComponent(chatId)}`, {
const data = await requestJson(getMothershipChatContract, {
params: { chatId },
query: windowStartCursor ? { from: windowStartCursor } : { window: 'latest' },
signal,
})

if (!copilotRes.ok) {
throw new Error('Failed to load chat')
}

return parseChatHistory(await copilotRes.json())
return parseChatHistory(data)
}

/**
Expand Down
8 changes: 0 additions & 8 deletions apps/sim/lib/api/contracts/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,6 @@ export const copilotToolExecuteInternalBodySchema = z
)
export type CopilotToolExecuteInternalBody = z.input<typeof copilotToolExecuteInternalBodySchema>

export const copilotChatGetQuerySchema = z
.object({
workflowId: z.string().optional(),
workspaceId: z.string().optional(),
chatId: z.string().optional(),
})
.passthrough()

export const copilotChatStreamQuerySchema = z.object({
streamId: z.string().optional().default(''),
after: z.string().optional().default(''),
Expand Down
9 changes: 0 additions & 9 deletions apps/sim/lib/api/contracts/mothership-chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,6 @@ export type MothershipExecuteBody = z.input<typeof mothershipExecuteBodySchema>

export const mothershipEventsQuerySchema = mothershipChatOwnerSchema

export const mothershipChatGetQuerySchema = z
.object({
workflowId: z.string().optional(),
workspaceId: z.string().optional(),
organizationId: z.string().optional(),
chatId: z.string().optional(),
})
.passthrough()

export const mothershipChatPostEnvelopeSchema = z
.object({
message: z.string().max(MAX_CHAT_MESSAGE_LENGTH).optional(),
Expand Down
41 changes: 39 additions & 2 deletions apps/sim/lib/mothership/chat/application/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getActivelyBannedUserIds } from '@/lib/auth/ban'
import type { WorkspaceAuthorizationContext } from '@/lib/core/application'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import { conversationModeSelection } from '@/lib/mothership/chat/intent'
import { authorizeChatWorkflowRead, isChatModeAvailable } from '@/lib/mothership/chat/lifecycle'
import { resolveActiveWorkspaceApplicationContext } from '@/lib/workspaces/application/context'

export type ChatOwnerContext =
Expand All @@ -25,6 +26,7 @@ export async function resolveOwnedChatContext(principal: Principal, chatId: stri
userId: copilotChats.userId,
workspaceId: copilotChats.workspaceId,
organizationId: copilotChats.organizationId,
workflowId: copilotChats.workflowId,
type: copilotChats.type,
mode: conversationModeSelection,
memorySpaceId: copilotChats.memorySpaceId,
Expand All @@ -34,7 +36,8 @@ export async function resolveOwnedChatContext(principal: Principal, chatId: stri
.limit(1)
if (
!chat ||
Boolean(chat.workspaceId) === Boolean(chat.organizationId) ||
(chat.workspaceId && chat.organizationId) ||
(!chat.workspaceId && !chat.organizationId && !chat.workflowId) ||
chat.userId !== resolvePrincipalSubjectUserId(principal) ||
(chat.organizationId && chat.type !== 'mothership')
) {
Expand All @@ -46,15 +49,49 @@ export async function resolveOwnedChatContext(principal: Principal, chatId: stri
const owner: ChatOwnerContext = chat.organizationId
? { organizationId: chat.organizationId }
: {
...(await resolveActiveWorkspaceApplicationContext(chat.workspaceId!)),
...(await resolveActiveWorkspaceApplicationContext(
await resolveChatWorkspaceId(chatId, chat)
)),
organizationId: undefined,
}
return {
...owner,
chatId,
userId: chat.userId,
workflowId: chat.workflowId,
type: chat.type,
mode: chat.mode,
memorySpaceId: chat.memorySpaceId ?? null,
}
}

/**
* A workflow chat row from before chats carried a workspace is scoped by its workflow's, while
* its owner may still read that workflow.
*/
async function resolveChatWorkspaceId(
chatId: string,
chat: { userId: string; workspaceId: string | null; workflowId: string | null }
): Promise<string> {
if (chat.workspaceId) return chat.workspaceId
const workflow = chat.workflowId
? await authorizeChatWorkflowRead(chatId, chat.userId, chat.workflowId)
: null
if (!workflow?.workspaceId) throw new OrchestrationError('not_found', 'Chat not found')
return workflow.workspaceId
}

/**
* Refuses a chat its owner may no longer read, as the chat detail read does: a workflow chat
* whose workflow they cannot read, or a workspace Plan chat while Plan mode is off for them.
*/
export async function requireOwnedChatReadable(
context: Awaited<ReturnType<typeof resolveOwnedChatContext>>
) {
if (
(context.workflowId &&
!(await authorizeChatWorkflowRead(context.chatId, context.userId, context.workflowId))) ||
!(await isChatModeAvailable(context, context.userId))
)
throw new OrchestrationError('not_found', 'Chat not found')
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { defineWorkspaceOperation } from '@/lib/core/application'
import { defineOrganizationOperation } from '@/lib/core/application/organization-operation'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import { defineAuthorizedChatUseCase } from '@/lib/mothership/chat/application/authorized-chat-use-case'
import { resolveOwnedChatContext } from '@/lib/mothership/chat/application/context'
import {
requireOwnedChatReadable,
resolveOwnedChatContext,
} from '@/lib/mothership/chat/application/context'
import { loadCopilotChatMessagesBefore } from '@/lib/mothership/chat/lifecycle'
import { compactToolCallParams, normalizeMessage } from '@/lib/mothership/chat/persisted-message'
import { decodeTranscriptCursor } from '@/lib/mothership/chat/transcript-cursor'
Expand All @@ -14,7 +17,7 @@ interface ReadChatOlderMessagesInput {
}

/**
* A page of the owner's mothership chat transcript before a cursor the chat detail read
* A page of the owner's chat transcript before a cursor the chat detail read
* issued, normalized and compacted exactly as that read returns its messages, so a message
* the backfill has not reached still arrives without its bulk tool-call params.
*/
Expand Down Expand Up @@ -47,7 +50,7 @@ export const readChatOlderMessages = defineAuthorizedChatUseCase({
async execute({ context, input }) {
const position = decodeTranscriptCursor(input.before)
if (!position) throw new OrchestrationError('validation', 'Invalid cursor')
if (context.type !== 'mothership') throw new OrchestrationError('not_found', 'Chat not found')
await requireOwnedChatReadable(context)
Comment thread
waleedlatif1 marked this conversation as resolved.
const page = await loadCopilotChatMessagesBefore(context.chatId, position)
return {
messages: page.messages.map((message) =>
Expand Down
Loading
Loading