From a451533ed5b777c9e5365bf67458418365b5022f Mon Sep 17 00:00:00 2001 From: Justin Blumencranz Date: Wed, 1 Jul 2026 13:55:27 -0700 Subject: [PATCH 01/10] feat(vfs): chat-scoped outputs/ namespace for one-off generated files --- apps/sim/app/api/files/authorization.ts | 28 ++- apps/sim/app/api/files/view/[id]/route.ts | 14 +- .../chats/[chatId]/outputs/route.ts | 47 +++++ .../workspaces/[id]/files/[fileId]/route.ts | 50 +++++ .../add-resource-dropdown.tsx | 30 ++- .../resource-content/resource-content.tsx | 58 +++++- .../resource-registry/resource-registry.tsx | 5 + .../resource-tabs/resource-tabs.tsx | 1 + .../app/workspace/[workspaceId]/home/home.tsx | 36 +++- apps/sim/hooks/queries/workspace-files.ts | 51 +++++ apps/sim/lib/cleanup/chat-cleanup.ts | 9 +- apps/sim/lib/copilot/request/lifecycle/run.ts | 6 + apps/sim/lib/copilot/tool-executor/types.ts | 7 + .../tools/handlers/materialize-file.ts | 51 ++++- .../tools/handlers/output-file-reader.test.ts | 160 +++++++++++++++ .../tools/handlers/output-file-reader.ts | 186 ++++++++++++++++++ apps/sim/lib/copilot/tools/handlers/vfs.ts | 87 +++++++- .../tools/registry/server-tool-adapter.ts | 1 + .../sim/lib/copilot/tools/server/base-tool.ts | 8 + .../copilot/tools/server/files/create-file.ts | 7 + .../files/download-to-workspace-file.ts | 7 + .../tools/server/files/workspace-file.ts | 18 ++ .../tools/server/image/generate-image.ts | 7 + .../lib/copilot/tools/server/media/ffmpeg.ts | 7 + .../tools/server/media/generate-audio.ts | 7 + .../tools/server/media/generate-video.ts | 7 + apps/sim/lib/copilot/vfs/resource-writer.ts | 69 ++++++- apps/sim/lib/uploads/config.ts | 2 + .../workspace/workspace-file-manager.ts | 170 +++++++++++++++- apps/sim/lib/uploads/server/metadata.ts | 13 +- apps/sim/lib/uploads/shared/types.ts | 1 + 31 files changed, 1098 insertions(+), 52 deletions(-) create mode 100644 apps/sim/app/api/mothership/chats/[chatId]/outputs/route.ts create mode 100644 apps/sim/lib/copilot/tools/handlers/output-file-reader.test.ts create mode 100644 apps/sim/lib/copilot/tools/handlers/output-file-reader.ts diff --git a/apps/sim/app/api/files/authorization.ts b/apps/sim/app/api/files/authorization.ts index 8e0c66b4173..8719e28bdb4 100644 --- a/apps/sim/app/api/files/authorization.ts +++ b/apps/sim/app/api/files/authorization.ts @@ -15,6 +15,23 @@ import { isUuid } from '@/executor/constants' const logger = createLogger('FileAuthorization') +/** + * Contexts this key lookup resolves for workspace-membership authorization: + * `workspace` (durable UI files) and `output` (chat-scoped agent outputs). Both + * share the `workspace//...` key shape and are authorized by workspace + * membership, so a by-key match on either is safe to grant to any member. + * + * `mothership` chat uploads ALSO share the bucket/key shape but are deliberately + * left OUT: today they authorize via the Priority-2 object-metadata fallback, and + * moving them onto this Priority-1 DB path would change how uploads work — an + * unrelated change deferred to its own PR (see outputs-vfs-followups.md #2). + * Owner-scoped contexts that also live in `workspace_files` (`copilot`, + * `knowledge-base`, `workspace-logos`) are excluded too: they have stricter + * per-owner rules and must never be granted through workspace membership on a + * key collision. + */ +const WORKSPACE_FILE_LOOKUP_CONTEXTS: StorageContext[] = ['workspace', 'output'] + /** Thrown by utility functions when file access is denied, so route handlers can return 404. */ export class FileAccessDeniedError extends Error { constructor() { @@ -54,8 +71,15 @@ async function lookupWorkspaceFileByKey( ): Promise<{ workspaceId: string; uploadedBy: string } | null> { try { const { includeDeleted = false } = options ?? {} - // Priority 1: Check new workspaceFiles table - const fileRecord = await getFileMetadataByKey(key, 'workspace', { includeDeleted }) + // Priority 1: Check new workspaceFiles table. Look up by key across + // WORKSPACE_FILE_LOOKUP_CONTEXTS (`workspace` + `output`): both share the + // `workspace//...` key shape and authorize by workspace membership. Filtering + // to `workspace` alone made `output` files unservable (broken previews); scoping to + // this explicit set (rather than dropping the filter) keeps outputs servable while + // leaving upload (`mothership`) authorization and the owner-scoped contexts untouched. + const fileRecord = await getFileMetadataByKey(key, WORKSPACE_FILE_LOOKUP_CONTEXTS, { + includeDeleted, + }) if (fileRecord) { return { diff --git a/apps/sim/app/api/files/view/[id]/route.ts b/apps/sim/app/api/files/view/[id]/route.ts index e9d68b86821..f3612617fd5 100644 --- a/apps/sim/app/api/files/view/[id]/route.ts +++ b/apps/sim/app/api/files/view/[id]/route.ts @@ -44,14 +44,16 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'Not found' }, { status: 404 }) } - // Only workspace-scoped files are embeddable/viewable here. Other contexts (e.g. chat-scoped - // `mothership` uploads) are not durable workspace artifacts; now that the caller is known to have - // access, reject with a legible 422 so the embed fails cleanly and the file agent can self-correct. - if (record.context !== 'workspace') { - logger.warn('Rejected non-workspace file view', { id, context: record.context }) + // Embeddable contexts: durable `workspace` files and chat-scoped `output` files + // (agent-generated one-offs the user previews inline before optionally saving to + // the workspace). Other contexts (e.g. `mothership` user uploads) are not embeddable + // here; now that the caller is known to have access, reject with a legible 422 so the + // embed fails cleanly and the file agent can self-correct. + if (record.context !== 'workspace' && record.context !== 'output') { + logger.warn('Rejected non-embeddable file view', { id, context: record.context }) return NextResponse.json( { - error: `File ${id} has context "${record.context}" and is not embeddable. Only workspace files can be viewed via /api/files/view. Save it into the workspace and reference the workspace copy.`, + error: `File ${id} has context "${record.context}" and is not embeddable. Only workspace and output files can be viewed via /api/files/view. Save it into the workspace and reference the workspace copy.`, }, { status: 422 } ) diff --git a/apps/sim/app/api/mothership/chats/[chatId]/outputs/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/outputs/route.ts new file mode 100644 index 00000000000..3f05fa4fad7 --- /dev/null +++ b/apps/sim/app/api/mothership/chats/[chatId]/outputs/route.ts @@ -0,0 +1,47 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { getAccessibleCopilotChatAuth } from '@/lib/copilot/chat/lifecycle' +import { + authenticateCopilotRequestSessionOnly, + createUnauthorizedResponse, +} from '@/lib/copilot/request/http' +import { listChatOutputs } from '@/lib/copilot/tools/handlers/output-file-reader' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('MothershipChatOutputsAPI') + +/** + * GET /api/mothership/chats/[chatId]/outputs + * + * List the chat-scoped `output` files (agent-generated one-offs) for a chat. These + * never appear in the workspace Files list (`listWorkspaceFiles` is workspace-only), + * so the resource panel uses this to show them alongside workspace files in the + * "+" resource picker and open them as tabs. Returns the same `WorkspaceFileRecord` + * shape as the workspace file list. + */ +export const GET = withRouteHandler( + async (_request: NextRequest, context: { params: Promise<{ chatId: string }> }) => { + try { + const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() + if (!isAuthenticated || !userId) { + return createUnauthorizedResponse() + } + + const { chatId } = await context.params + const chat = await getAccessibleCopilotChatAuth(chatId, userId) + if (!chat) { + return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 }) + } + + const files = await listChatOutputs(chatId) + return NextResponse.json({ success: true, files }) + } catch (error) { + logger.error('Failed to list chat outputs', error) + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Failed to list chat outputs') }, + { status: 500 } + ) + } + } +) diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts index 1826988ea08..3f5e921ab5d 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts @@ -14,12 +14,62 @@ import { performDeleteWorkspaceFileItems, performRenameWorkspaceFile, } from '@/lib/workspace-files/orchestration' +import { getPreviewableWorkspaceFile } from '@/lib/uploads/contexts/workspace' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' export const dynamic = 'force-dynamic' const logger = createLogger('WorkspaceFileAPI') +/** + * GET /api/workspaces/[id]/files/[fileId] + * Fetch a single file record by id, including chat-scoped `output` files that never + * appear in the workspace Files list. Used by the resource panel to preview an output + * the list-based lookup can't see. Requires workspace membership (read). + */ +export const GET = withRouteHandler( + async (request: NextRequest, { params }: { params: Promise<{ id: string; fileId: string }> }) => { + const requestId = generateRequestId() + const paramsResult = workspaceFileParamsSchema.safeParse(await params) + if (!paramsResult.success) { + return NextResponse.json( + { error: getValidationErrorMessage(paramsResult.error, 'Invalid route parameters') }, + { status: 400 } + ) + } + const { id: workspaceId, fileId } = paramsResult.data + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userPermission = await getUserEntityPermissions( + session.user.id, + 'workspace', + workspaceId + ) + if (userPermission === null) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + const file = await getPreviewableWorkspaceFile(workspaceId, fileId) + if (!file) { + return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 }) + } + + return NextResponse.json({ success: true, file }) + } catch (error) { + logger.error(`[${requestId}] Error fetching workspace file:`, error) + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Failed to fetch file') }, + { status: 500 } + ) + } + } +) + /** * PATCH /api/workspaces/[id]/files/[fileId] * Rename a workspace file (requires write permission) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx index 7f67d25704f..2736a9603f3 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx @@ -35,7 +35,7 @@ import { useWorkspaceSchedules } from '@/hooks/queries/schedules' import { useTablesList } from '@/hooks/queries/tables' import { useWorkflows } from '@/hooks/queries/workflows' import { useWorkspaceFileFolders } from '@/hooks/queries/workspace-file-folders' -import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' +import { useChatOutputs, useWorkspaceFiles } from '@/hooks/queries/workspace-files' export interface AddResourceDropdownProps { workspaceId: string @@ -44,6 +44,8 @@ export interface AddResourceDropdownProps { onSwitch?: (resourceId: string) => void /** Resource types to hide from the dropdown (e.g. `['folder', 'task']`). */ excludeTypes?: readonly MothershipResourceType[] + /** Active chat id; when set, this chat's `output` files are listed under Files too. */ + chatId?: string } export type AvailableItem = { id: string; name: string; isOpen?: boolean; [key: string]: unknown } @@ -70,11 +72,15 @@ const LOG_DROPDOWN_FILTERS = { export function useAvailableResources( workspaceId: string, existingKeys: Set, - excludeTypes?: readonly MothershipResourceType[] + excludeTypes?: readonly MothershipResourceType[], + chatId?: string ): AvailableItemsByType[] { const { data: workflows = [] } = useWorkflows(workspaceId) const { data: tables = [] } = useTablesList(workspaceId) const { data: files = [] } = useWorkspaceFiles(workspaceId) + // Chat-scoped agent outputs aren't in the workspace list; surface them here so a + // generated `outputs/` file is pickable in the "+" menu and openable as a tab. + const { data: chatOutputs = [] } = useChatOutputs(chatId) const { data: knowledgeBases } = useKnowledgeBasesQuery(workspaceId) const { data: folders = [] } = useFolders(workspaceId) const { data: fileFolders = [] } = useWorkspaceFileFolders(workspaceId) @@ -85,6 +91,12 @@ export function useAvailableResources( return useMemo(() => { const excluded = new Set(excludeTypes ?? []) + // Merge workspace files with this chat's outputs, deduped by id (a materialized + // output becomes a workspace file with the same id, so it must not appear twice). + const fileList = + chatOutputs.length > 0 + ? [...files, ...chatOutputs.filter((o) => !files.some((f) => f.id === o.id))] + : files const groups: AvailableItemsByType[] = [ { type: 'workflow' as const, @@ -116,7 +128,7 @@ export function useAvailableResources( }, { type: 'file' as const, - items: files.map((f) => ({ + items: fileList.map((f) => ({ id: f.id, name: f.name, folderId: f.folderId ?? null, @@ -190,6 +202,7 @@ export function useAvailableResources( fileFolders, tables, files, + chatOutputs, knowledgeBases, tasks, schedules, @@ -387,14 +400,17 @@ export function AddResourceDropdown({ onAdd, onSwitch, excludeTypes, + chatId, }: AddResourceDropdownProps) { const [open, setOpen] = useState(false) const [search, setSearch] = useState('') const [activeIndex, setActiveIndex] = useState(0) - const available = useAvailableResources(workspaceId, existingKeys, [ - ...(excludeTypes ?? []), - 'integration', - ]) + const available = useAvailableResources( + workspaceId, + existingKeys, + [...(excludeTypes ?? []), 'integration'], + chatId + ) const handleOpenChange = (next: boolean) => { setOpen(next) if (!next) { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index 0bfe6bdb233..8ed75977b59 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -56,7 +56,11 @@ import { useLogDetail } from '@/hooks/queries/logs' import { useScheduleById } from '@/hooks/queries/schedules' import { downloadTableExport } from '@/hooks/queries/tables' import { useWorkflows } from '@/hooks/queries/workflows' -import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' +import { + useChatOutputs, + useWorkspaceFileById, + useWorkspaceFiles, +} from '@/hooks/queries/workspace-files' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' import { useExecutionStore } from '@/stores/execution/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -507,8 +511,8 @@ interface EmbeddedFileActionsProps { function EmbeddedFileActions({ workspaceId, fileId, filePath }: EmbeddedFileActionsProps) { const router = useRouter() - const { data: files = [] } = useWorkspaceFiles(workspaceId) - const file = useMemo( + const { data: files = [], isLoading, isFetching } = useWorkspaceFiles(workspaceId) + const listFile = useMemo( () => files.find( (f) => @@ -518,6 +522,14 @@ function EmbeddedFileActions({ workspaceId, fileId, filePath }: EmbeddedFileActi ), [files, fileId, filePath] ) + // Mirror EmbeddedFile: chat-scoped `output` files aren't in the list, so fall back to + // a by-id fetch when the list settles without a hit — keeps Download working for them. + const { data: fallbackFile = null } = useWorkspaceFileById( + workspaceId, + fileId, + !isLoading && !isFetching && !listFile + ) + const file = listFile ?? fallbackFile const handleDownload = async () => { if (!file) return @@ -629,7 +641,7 @@ function EmbeddedFile({ }: EmbeddedFileProps) { const { canEdit } = useUserPermissionsContext() const { data: files = [], isLoading, isFetching } = useWorkspaceFiles(workspaceId) - const file = useMemo( + const listFile = useMemo( () => files.find( (f) => @@ -640,7 +652,43 @@ function EmbeddedFile({ [files, fileId, filePath] ) - if (isLoading || (isFetching && !file)) return LOADING_SKELETON + // Chat-scoped `output` files are excluded from the workspace Files list, so the + // lookup above misses them. When the list has settled without a hit, fall back to a + // direct by-id fetch (which includes outputs) so the panel can still preview them. + const listSettled = !isLoading && !isFetching + const { data: fallbackFile = null, isLoading: fallbackLoading } = useWorkspaceFileById( + workspaceId, + fileId, + listSettled && !listFile + ) + + // The agent may reference an output by PATH (e.g. an `outputs/generated-image.jpg` link) + // rather than by id, in which case both the list and the by-id fetch miss (by-id matches + // on the wf_ id, not a path). Resolve it against this chat's outputs by leaf name. + // `previewContextKey` is the active chat id in this surface. + const { data: chatOutputs = [], isLoading: outputsLoading } = useChatOutputs(previewContextKey) + const outputFallback = useMemo(() => { + if (listFile || fallbackFile) return null + const ref = filePath ?? (fileId || '') + if (!ref) return null + const rawLeaf = ref.split('/').pop() ?? '' + let leaf = rawLeaf + try { + leaf = decodeURIComponent(rawLeaf) + } catch { + leaf = rawLeaf + } + return chatOutputs.find((o) => o.id === fileId || o.name === leaf || o.name === rawLeaf) ?? null + }, [listFile, fallbackFile, chatOutputs, filePath, fileId]) + + const file = listFile ?? fallbackFile ?? outputFallback + + if ( + isLoading || + (isFetching && !file) || + (listSettled && !listFile && (fallbackLoading || outputsLoading)) + ) + return LOADING_SKELETON if (!file) { return ( diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx index 5fbf767a4c0..b179e00aa49 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx @@ -234,6 +234,11 @@ const RESOURCE_INVALIDATORS: Record< qc.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) qc.invalidateQueries({ queryKey: workspaceFilesKeys.contentFile(wId, id) }) qc.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() }) + // Chat-scoped outputs live under their own key (not `lists()`); a generated + // output file must refresh them or `outputs/...` path references (e.g. the + // agent's #wsres-file links) can't resolve to the new file's id until the + // 30s staleTime lapses — the "File not found on first click" bug. + qc.invalidateQueries({ queryKey: workspaceFilesKeys.chatOutputsAll() }) }, workflow: (qc, wId) => { void invalidateWorkflowLists(qc, wId) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx index a99416db8de..395bf530292 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx @@ -622,6 +622,7 @@ export function ResourceTabs({ onAdd={handleAdd} onSwitch={selectResource} excludeTypes={ADD_RESOURCE_EXCLUDED_TYPES} + chatId={chatId} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 33b5f2c5f2f..bf3e5303732 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -44,7 +44,7 @@ import { useMothershipChatHistory, } from '@/hooks/queries/mothership-chats' import { useWorkflows } from '@/hooks/queries/workflows' -import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' +import { useChatOutputs, useWorkspaceFiles } from '@/hooks/queries/workspace-files' import { useOAuthReturnRouter } from '@/hooks/use-oauth-return' import type { ChatContext } from '@/stores/panel' import { @@ -124,6 +124,9 @@ export function Home({ chatId, userName, userId }: HomeProps) { ) const firstName = userName?.split(' ')[0] ?? '' const { data: workspaceFiles = [] } = useWorkspaceFiles(workspaceId) + // Chat-scoped agent outputs aren't in the workspace list; used to resolve an + // `outputs/...` file reference (e.g. a #wsres-file link) to its real file id. + const { data: chatOutputs = [] } = useChatOutputs(chatId) const { data: workflows = [] } = useWorkflows(workspaceId) const { data: folders = [] } = useFolders(workspaceId) const posthog = usePostHog() @@ -418,15 +421,32 @@ export function Home({ chatId, userName, userId }: HomeProps) { ) }) - if (!file) return resource - return { - ...resource, - id: file.id, - title: resource.title || file.name, - path: alias ? reference : resource.path, + if (file) { + return { + ...resource, + id: file.id, + title: resource.title || file.name, + path: alias ? reference : resource.path, + } + } + + // Not a workspace file — try this chat's outputs (excluded from the workspace + // list). Resolving to the real file id lets the panel open it normally and + // de-dupes against the tab the generator auto-opened. + let leaf = reference.split('/').pop() ?? reference + try { + leaf = decodeURIComponent(leaf) + } catch { + // keep raw leaf + } + const output = chatOutputs.find((o) => o.id === reference || o.name === leaf) + if (output) { + return { ...resource, id: output.id, title: resource.title || output.name } } + + return resource }, - [workflowAliasEntries, workspaceFiles] + [workflowAliasEntries, workspaceFiles, chatOutputs] ) function handleWorkspaceResourceSelect(resource: MothershipResource) { diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index d03a80c50f2..a426caaef81 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -52,6 +52,8 @@ export const workspaceFilesKeys = { ...(storageKey ? [storageKey] : []), ] as const, storageInfo: () => [...workspaceFilesKeys.all, 'storageInfo'] as const, + chatOutputsAll: () => [...workspaceFilesKeys.all, 'chatOutputs'] as const, + chatOutputs: (chatId: string) => [...workspaceFilesKeys.chatOutputsAll(), chatId] as const, } /** @@ -81,6 +83,55 @@ export function useWorkspaceFileRecord(workspaceId: string, fileId: string) { }) } +/** + * List the chat-scoped `output` files (agent-generated one-offs) for a chat. These are + * NOT in {@link useWorkspaceFiles} (which is workspace-only), so the resource panel uses + * this to surface them alongside workspace files in the "+" picker and as open tabs. + * Returns the same `WorkspaceFileRecord` shape as the workspace file list. + */ +export function useChatOutputs(chatId: string | undefined) { + return useQuery({ + queryKey: workspaceFilesKeys.chatOutputs(chatId ?? ''), + queryFn: async ({ signal }): Promise => { + if (!chatId) return [] + // boundary-raw-fetch: chat-scoped outputs list; no contract + const response = await fetch( + `/api/mothership/chats/${encodeURIComponent(chatId)}/outputs`, + { signal, cache: 'no-store' } + ) + if (!response.ok) return [] + const data = await response.json().catch(() => null) + return data?.success && Array.isArray(data.files) ? (data.files as WorkspaceFileRecord[]) : [] + }, + enabled: !!chatId, + staleTime: 30 * 1000, + }) +} + +/** + * Fallback hook to fetch a single file record by id directly from the API, including + * chat-scoped `output` files that never appear in the workspace Files list (and so are + * absent from {@link useWorkspaceFiles}). The resource panel uses this only when the + * list lookup misses, so a generated `outputs/` file can still be previewed. + */ +export function useWorkspaceFileById(workspaceId: string, fileId: string, enabled: boolean) { + return useQuery({ + queryKey: [...workspaceFilesKeys.all, 'byId', workspaceId, fileId] as const, + queryFn: async ({ signal }): Promise => { + // boundary-raw-fetch: single-file record lookup incl. chat-scoped outputs; no contract + const response = await fetch( + `/api/workspaces/${encodeURIComponent(workspaceId)}/files/${encodeURIComponent(fileId)}`, + { signal, cache: 'no-store' } + ) + if (!response.ok) return null + const data = await response.json().catch(() => null) + return data?.success && data.file ? (data.file as WorkspaceFileRecord) : null + }, + enabled: enabled && !!workspaceId && !!fileId, + staleTime: 30 * 1000, + }) +} + /** * Fetch workspace files from API */ diff --git a/apps/sim/lib/cleanup/chat-cleanup.ts b/apps/sim/lib/cleanup/chat-cleanup.ts index c5dafdf9c27..632cad282e4 100644 --- a/apps/sim/lib/cleanup/chat-cleanup.ts +++ b/apps/sim/lib/cleanup/chat-cleanup.ts @@ -19,8 +19,15 @@ const CHAT_FILE_COLLECT_CHUNK_SIZE = 500 * files, execution logs, knowledge bases, profile pictures, etc. are owned by * other subsystems and must never be touched by chat cleanup — even if a row * somehow ends up with `chatId` set through a future flow. + * + * - `copilot` / `mothership`: user uploads attached to a chat. + * - `output`: agent-generated one-off files written to the `outputs/` VFS namespace. */ -const CHAT_SCOPED_CONTEXTS = ['copilot', 'mothership'] as const satisfies readonly StorageContext[] +const CHAT_SCOPED_CONTEXTS = [ + 'copilot', + 'mothership', + 'output', +] as const satisfies readonly StorageContext[] type ChatScopedContext = (typeof CHAT_SCOPED_CONTEXTS)[number] interface FileRef { diff --git a/apps/sim/lib/copilot/request/lifecycle/run.ts b/apps/sim/lib/copilot/request/lifecycle/run.ts index 12af8bfe89d..1b6d2ec1844 100644 --- a/apps/sim/lib/copilot/request/lifecycle/run.ts +++ b/apps/sim/lib/copilot/request/lifecycle/run.ts @@ -134,6 +134,12 @@ export async function runCopilotLifecycle( abortSignal: lifecycleOptions.abortSignal, })) + // Only genuine interactive turns have a persisted `copilot_chats` row; headless + // runs (e.g. Mothership block execution) carry an ephemeral, non-persisted + // chatId. Server tools gate chat-scoped `outputs/` writes on this so they never + // attempt a `chat_id` FK insert against a chat that does not exist. + execContext.interactive = lifecycleOptions.interactive === true + const context = createStreamingContext({ chatId, requestId: lifecycleOptions.simRequestId, diff --git a/apps/sim/lib/copilot/tool-executor/types.ts b/apps/sim/lib/copilot/tool-executor/types.ts index f1c2eae27f5..94b688c3ab0 100644 --- a/apps/sim/lib/copilot/tool-executor/types.ts +++ b/apps/sim/lib/copilot/tool-executor/types.ts @@ -11,6 +11,13 @@ export interface ToolExecutionContext { copilotToolExecution?: boolean requestMode?: string currentAgentId?: string + /** + * True only for genuine interactive chat turns (which always have a persisted + * `copilot_chats` row). Undefined/false for headless runs (e.g. Mothership + * block execution) whose `chatId` is ephemeral and not persisted. Gates + * chat-scoped `outputs/` writes, which carry a `chat_id` FK to `copilot_chats`. + */ + interactive?: boolean /** * The invoking subagent's channel id (its outer tool_use id), threaded per * tool call so server tools can scope state to one subagent invocation. Two diff --git a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts index a514f6f735c..32eeef8ce63 100644 --- a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts +++ b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts @@ -6,10 +6,14 @@ import { getErrorMessage, toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, eq, isNull } from 'drizzle-orm' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { findChatOutputRowByChatAndName } from '@/lib/copilot/tools/handlers/output-file-reader' import { findMothershipUploadRowByChatAndName } from '@/lib/copilot/tools/handlers/upload-file-reader' import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils' import { getServePathPrefix } from '@/lib/uploads' -import { fetchWorkspaceFileBuffer } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { + allocateUniqueWorkspaceFileName, + fetchWorkspaceFileBuffer, +} from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { parseWorkflowJson } from '@/lib/workflows/operations/import-export' import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' import { deduplicateWorkflowName } from '@/lib/workflows/utils' @@ -36,17 +40,37 @@ function toFileRecord(row: typeof workspaceFiles.$inferSelect) { } async function executeSave(fileName: string, chatId: string): Promise { - const row = await findMothershipUploadRowByChatAndName(chatId, fileName) + // `save` promotes a chat-scoped file into the permanent workspace. The source is + // either a user upload (`uploads/`, context 'mothership') or an agent-generated + // one-off output (`outputs/`, context 'output') — both live in workspace_files and + // promote identically (flip context to 'workspace', detach from the chat). + const row = + (await findMothershipUploadRowByChatAndName(chatId, fileName)) ?? + (await findChatOutputRowByChatAndName(chatId, fileName)) if (!row) { return { success: false, - error: `Upload not found: "${fileName}". Use glob("uploads/*") to list available uploads.`, + error: `File not found: "${fileName}". Use glob("uploads/*") or glob("outputs/*") to list available chat files.`, } } + // Chat-scoped names are unique only within their chat, so two chats can each hold a + // "generated-image.jpg". Promoting both to the workspace root would collide on the + // `workspace_files_workspace_folder_name_active_unique` index (context='workspace'), + // so disambiguate against existing workspace files first (e.g. "generated-image (1).jpg"). + const desiredName = row.displayName ?? row.originalName + const uniqueName = row.workspaceId + ? await allocateUniqueWorkspaceFileName(row.workspaceId, desiredName, row.folderId ?? null) + : desiredName + const [updated] = await db .update(workspaceFiles) - .set({ context: 'workspace', chatId: null, originalName: row.displayName ?? row.originalName }) + .set({ + context: 'workspace', + chatId: null, + originalName: uniqueName, + displayName: uniqueName, + }) .where(and(eq(workspaceFiles.id, row.id), isNull(workspaceFiles.deletedAt))) .returning({ id: workspaceFiles.id, originalName: workspaceFiles.originalName }) @@ -71,6 +95,10 @@ async function executeSave(fileName: string, chatId: string): Promise = [] const failed: Array<{ fileName: string; error: string }> = [] const resources: NonNullable = [] @@ -230,7 +260,16 @@ export async function executeMaterializeFile( } if (result.success) { - succeeded.push(fileName) + const out = (result.output ?? {}) as { + name?: string + path?: string + workflowName?: string + } + succeeded.push({ + requested: fileName, + name: out.name ?? out.workflowName ?? fileName, + ...(out.path ? { path: out.path } : {}), + }) if (result.resources) resources.push(...result.resources) } else { failed.push({ fileName, error: result.error ?? 'Failed to materialize file' }) diff --git a/apps/sim/lib/copilot/tools/handlers/output-file-reader.test.ts b/apps/sim/lib/copilot/tools/handlers/output-file-reader.test.ts new file mode 100644 index 00000000000..db19913779e --- /dev/null +++ b/apps/sim/lib/copilot/tools/handlers/output-file-reader.test.ts @@ -0,0 +1,160 @@ +/** + * @vitest-environment node + */ + +import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@sim/db', () => dbChainMock) + +const { mockReadFileRecord } = vi.hoisted(() => ({ + mockReadFileRecord: vi.fn(), +})) + +vi.mock('@/lib/copilot/vfs/file-reader', () => ({ + readFileRecord: mockReadFileRecord, +})) + +import { + findChatOutputRowByChatAndName, + listChatOutputs, + readChatOutput, +} from './output-file-reader' + +const CHAT_ID = '11111111-1111-1111-1111-111111111111' +const NOW = new Date('2026-05-05T00:00:00.000Z') + +function makeRow(overrides: Partial> = {}) { + return { + id: 'wf_1', + key: 'workspace/ws_1/123-chart.png', + userId: 'user_1', + workspaceId: 'ws_1', + context: 'output', + chatId: CHAT_ID, + originalName: 'chart.png', + displayName: 'chart.png', + contentType: 'image/png', + size: 1024, + deletedAt: null, + uploadedAt: NOW, + updatedAt: NOW, + ...overrides, + } +} + +/** + * Resolver chain is `.where().orderBy(...).limit(1)`. The default chain mock makes + * `orderBy` a terminal, so we wire a chainable `{limit}` for each call manually. + */ +function mockOrderByThenLimit(rows: unknown) { + dbChainMockFns.orderBy.mockReturnValueOnce({ limit: dbChainMockFns.limit } as never) + dbChainMockFns.limit.mockResolvedValueOnce(rows as never) +} + +describe('findChatOutputRowByChatAndName', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('matches by displayName for the first occurrence', async () => { + const row = makeRow({ id: 'wf_1', displayName: 'chart.png' }) + mockOrderByThenLimit([row]) + + const result = await findChatOutputRowByChatAndName(CHAT_ID, 'chart.png') + + expect(result).toEqual(row) + }) + + it('matches by suffixed displayName for collision-disambiguated rows', async () => { + const row = makeRow({ id: 'wf_2', displayName: 'chart (2).png' }) + mockOrderByThenLimit([row]) + + const result = await findChatOutputRowByChatAndName(CHAT_ID, 'chart (2).png') + + expect(result?.id).toBe('wf_2') + expect(result?.displayName).toBe('chart (2).png') + }) + + it('returns null when no row matches and the fallback scan is empty', async () => { + // First query: .where().orderBy().limit() returns []. + mockOrderByThenLimit([]) + // Second query: .where().orderBy(...) (no .limit) — orderBy is the terminal. + dbChainMockFns.orderBy.mockResolvedValueOnce([] as never) + + const result = await findChatOutputRowByChatAndName(CHAT_ID, 'missing.png') + + expect(result).toBeNull() + }) + + it('falls back to a normalized segment match when the exact lookup misses', async () => { + const row = makeRow({ id: 'wf_3', displayName: 'My Chart.png' }) + + mockOrderByThenLimit([]) + dbChainMockFns.orderBy.mockResolvedValueOnce([row] as never) + + const result = await findChatOutputRowByChatAndName(CHAT_ID, 'My%20Chart.png') + + expect(result?.id).toBe('wf_3') + }) +}) + +describe('listChatOutputs', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('returns rows in creation order with name set to displayName and output storage context', async () => { + const rows = [ + makeRow({ id: 'a', displayName: 'chart.png' }), + makeRow({ id: 'b', displayName: 'chart (2).png' }), + makeRow({ id: 'c', displayName: 'chart (3).png' }), + ] + dbChainMockFns.orderBy.mockResolvedValueOnce(rows) + + const result = await listChatOutputs(CHAT_ID) + + expect(result.map((r) => r.id)).toEqual(['a', 'b', 'c']) + expect(result.map((r) => r.name)).toEqual(['chart.png', 'chart (2).png', 'chart (3).png']) + expect(result.every((r) => r.storageContext === 'output')).toBe(true) + }) + + it('returns [] and does not throw when the DB query fails', async () => { + dbChainMockFns.orderBy.mockRejectedValueOnce(new Error('boom')) + const result = await listChatOutputs(CHAT_ID) + expect(result).toEqual([]) + }) +}) + +describe('readChatOutput', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mockReadFileRecord.mockReset() + }) + + it('reads the row resolved by the suffixed displayName', async () => { + const row = makeRow({ id: 'wf_2', displayName: 'chart (2).png' }) + mockOrderByThenLimit([row]) + mockReadFileRecord.mockResolvedValueOnce({ content: 'PNGDATA', totalLines: 1 }) + + const result = await readChatOutput('chart (2).png', CHAT_ID) + + expect(result).toEqual({ content: 'PNGDATA', totalLines: 1 }) + expect(mockReadFileRecord).toHaveBeenCalledWith( + expect.objectContaining({ id: 'wf_2', name: 'chart (2).png', storageContext: 'output' }) + ) + }) + + it('returns null when no row matches', async () => { + mockOrderByThenLimit([]) + dbChainMockFns.orderBy.mockResolvedValueOnce([] as never) + + const result = await readChatOutput('nope.png', CHAT_ID) + + expect(result).toBeNull() + expect(mockReadFileRecord).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/copilot/tools/handlers/output-file-reader.ts b/apps/sim/lib/copilot/tools/handlers/output-file-reader.ts new file mode 100644 index 00000000000..77fdea16706 --- /dev/null +++ b/apps/sim/lib/copilot/tools/handlers/output-file-reader.ts @@ -0,0 +1,186 @@ +import { db } from '@sim/db' +import { workspaceFiles } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { and, asc, desc, eq, isNull, or } from 'drizzle-orm' +import { type FileReadResult, readFileRecord } from '@/lib/copilot/vfs/file-reader' +import { + type GrepCountEntry, + type GrepMatch, + type GrepOptions, + grepReadResult, + WorkspaceFileGrepError, +} from '@/lib/copilot/vfs/operations' +import { decodeVfsSegment, encodeVfsSegment } from '@/lib/copilot/vfs/path-utils' +import { getServePathPrefix } from '@/lib/uploads' +import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager' + +const logger = createLogger('OutputFileReader') + +/** + * Chat-scoped agent outputs (`context='output'`) are the write side's twin of chat + * uploads (`context='mothership'`): same `workspace_files` table and workspace bucket, + * but generated by the AGENT for one-off files instead of uploaded by the user. This + * module is the read side, mirroring upload-file-reader.ts for the `outputs/` namespace. + */ + +/** + * Canonical comparison key for an output's VFS name. Accepts both the raw display + * name and a percent-encoded segment so either spelling resolves the same row. + */ +function canonicalOutputKey(name: string): string { + let decoded = name + try { + decoded = decodeVfsSegment(name) + } catch { + decoded = name + } + try { + return encodeVfsSegment(decoded) + } catch { + return name.trim() + } +} + +/** VFS-visible name. Coalesces to originalName for safety, though outputs always set displayName. */ +function vfsName(row: typeof workspaceFiles.$inferSelect): string { + return row.displayName ?? row.originalName +} + +function toWorkspaceFileRecord(row: typeof workspaceFiles.$inferSelect): WorkspaceFileRecord { + const pathPrefix = getServePathPrefix() + return { + id: row.id, + workspaceId: row.workspaceId || '', + name: vfsName(row), + key: row.key, + path: `${pathPrefix}${encodeURIComponent(row.key)}?context=output`, + size: row.size, + type: row.contentType, + uploadedBy: row.userId, + deletedAt: row.deletedAt, + uploadedAt: row.uploadedAt, + updatedAt: row.updatedAt, + storageContext: 'output', + } +} + +/** + * Resolve a chat-scoped output row by VFS name. Prefers an exact DB match; falls back + * to a normalized scan (encoded vs decoded names). On ambiguity returns the most recent. + */ +export async function findChatOutputRowByChatAndName( + chatId: string, + fileName: string +): Promise { + const exactRows = await db + .select() + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.chatId, chatId), + eq(workspaceFiles.context, 'output'), + or( + eq(workspaceFiles.displayName, fileName), + and(isNull(workspaceFiles.displayName), eq(workspaceFiles.originalName, fileName)) + ), + isNull(workspaceFiles.deletedAt) + ) + ) + .orderBy(desc(workspaceFiles.uploadedAt), desc(workspaceFiles.id)) + .limit(1) + + if (exactRows[0]) { + return exactRows[0] + } + + const allRows = await db + .select() + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.chatId, chatId), + eq(workspaceFiles.context, 'output'), + isNull(workspaceFiles.deletedAt) + ) + ) + .orderBy(desc(workspaceFiles.uploadedAt), desc(workspaceFiles.id)) + + const segmentKey = canonicalOutputKey(fileName) + return allRows.find((r) => canonicalOutputKey(vfsName(r)) === segmentKey) ?? null +} + +/** + * List all chat-scoped outputs for a given chat in creation order. + */ +export async function listChatOutputs(chatId: string): Promise { + try { + const rows = await db + .select() + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.chatId, chatId), + eq(workspaceFiles.context, 'output'), + isNull(workspaceFiles.deletedAt) + ) + ) + .orderBy(asc(workspaceFiles.uploadedAt), asc(workspaceFiles.id)) + + return rows.map(toWorkspaceFileRecord) + } catch (err) { + logger.warn('Failed to list chat outputs', { + chatId, + error: toError(err).message, + }) + return [] + } +} + +/** + * Read a specific output file by display name within a chat session. + */ +export async function readChatOutput( + filename: string, + chatId: string +): Promise { + try { + const row = await findChatOutputRowByChatAndName(chatId, filename) + if (!row) return null + return readFileRecord(toWorkspaceFileRecord(row)) + } catch (err) { + logger.warn('Failed to read chat output', { + filename, + chatId, + error: toError(err).message, + }) + return null + } +} + +/** + * Grep the content of a single chat output (`outputs/`), mirroring + * {@link grepChatUpload}. Resolves the output by name, reads its text per file type, + * and greps it. Throws {@link WorkspaceFileGrepError} when the output is missing or + * has no searchable text so the caller surfaces the message verbatim. + */ +export async function grepChatOutput( + filename: string, + chatId: string, + pattern: string, + options?: GrepOptions +): Promise { + const row = await findChatOutputRowByChatAndName(chatId, filename) + if (!row) { + throw new WorkspaceFileGrepError( + `Output not found: "${filename}". Use glob("outputs/*") to list available outputs.` + ) + } + const record = toWorkspaceFileRecord(row) + const result = await readFileRecord(record) + if (!result) { + throw new WorkspaceFileGrepError(`Output content not found for "${filename}".`) + } + const outputsPath = `outputs/${canonicalOutputKey(record.name)}` + return grepReadResult(outputsPath, result, pattern, outputsPath, options) +} diff --git a/apps/sim/lib/copilot/tools/handlers/vfs.ts b/apps/sim/lib/copilot/tools/handlers/vfs.ts index ca1902692e1..fd9f652ac8b 100644 --- a/apps/sim/lib/copilot/tools/handlers/vfs.ts +++ b/apps/sim/lib/copilot/tools/handlers/vfs.ts @@ -6,6 +6,7 @@ import { getOrMaterializeVFS } from '@/lib/copilot/vfs' import type { GrepCountEntry, GrepMatch } from '@/lib/copilot/vfs/operations' import { WorkspaceFileGrepError } from '@/lib/copilot/vfs/operations' import { encodeVfsSegment } from '@/lib/copilot/vfs/path-utils' +import { grepChatOutput, listChatOutputs, readChatOutput } from './output-file-reader' import { grepChatUpload, listChatUploads, readChatUpload } from './upload-file-reader' const logger = createLogger('VfsTools') @@ -40,6 +41,12 @@ function isChatUploadGrepPath(path: string | undefined): path is string { return /^uploads(\/|$)/.test(path.replace(/^\/+/, '')) } +/** True when a grep `path` targets the chat-scoped outputs namespace. */ +function isChatOutputGrepPath(path: string | undefined): path is string { + if (!path) return false + return /^outputs(\/|$)/.test(path.replace(/^\/+/, '')) +} + function serializedResultSize(value: unknown): number { try { return JSON.stringify(value).length @@ -99,25 +106,36 @@ export async function executeVfsGrep( // Chat uploads are opt-in like recently-deleted/: they are never in the VFS // map, so an unscoped grep can't touch them — only an explicit uploads/ // path does, and only one upload at a time. + // uploads/ and outputs/ are both chat-scoped, single-file content greps (opt-in, + // never in the VFS map). Resolve which namespace once — computing it in a ternary + // (not chained if/else type guards) keeps rawPath typed as string | undefined. + const chatScopedNamespace: 'uploads' | 'outputs' | null = isChatUploadGrepPath(rawPath) + ? 'uploads' + : isChatOutputGrepPath(rawPath) + ? 'outputs' + : null + let result: GrepMatch[] | string[] | GrepCountEntry[] - if (isChatUploadGrepPath(rawPath)) { + if (chatScopedNamespace) { if (!context.chatId) { - return { success: false, error: 'No chat context available for uploads/' } + return { success: false, error: `No chat context available for ${chatScopedNamespace}/` } } - // The upload is the first segment after uploads/; any trailing segment - // (e.g. a /content suffix) is ignored, mirroring the uploads read path. - const filename = rawPath + // The file is the first segment after the namespace; any trailing segment + // (e.g. a /content suffix) is ignored, mirroring the read path. + const filename = (rawPath ?? '') .replace(/^\/+/, '') - .replace(/^uploads\/?/, '') + .replace(chatScopedNamespace === 'uploads' ? /^uploads\/?/ : /^outputs\/?/, '') .split('/')[0] if (!filename) { return { success: false, - error: - 'Grep over chat uploads must target a single upload (e.g. path: "uploads/report.json"). Use glob("uploads/*") to list uploads.', + error: `Grep over chat ${chatScopedNamespace} must target a single file (e.g. path: "${chatScopedNamespace}/report.json"). Use glob("${chatScopedNamespace}/*") to list them.`, } } - result = await grepChatUpload(filename, context.chatId, pattern, grepOptions) + result = + chatScopedNamespace === 'uploads' + ? await grepChatUpload(filename, context.chatId, pattern, grepOptions) + : await grepChatOutput(filename, context.chatId, pattern, grepOptions) } else { const vfs = await getOrMaterializeVFS(workspaceId, context.userId) result = isWorkspaceFileGrepPath(rawPath) @@ -187,6 +205,13 @@ export async function executeVfsGlob( files = [...files, ...uploadPaths] } + // Chat outputs are opt-in like uploads: only explicit outputs/ patterns include them. + if (context.chatId && (pattern === 'outputs/*' || pattern.startsWith('outputs/'))) { + const outputs = await listChatOutputs(context.chatId) + const outputPaths = outputs.map((f) => `outputs/${encodeUploadSegment(f.name)}`) + files = [...files, ...outputPaths] + } + logger.debug('vfs_glob result', { pattern, fileCount: files.length }) return { success: true, output: { files } } } catch (err) { @@ -281,6 +306,50 @@ export async function executeVfsRead( } } + // Chat-scoped agent outputs via the outputs/ virtual prefix (twin of uploads/). + // Flat and read directly; any trailing segment after the leaf is ignored. + if (path.startsWith('outputs/')) { + if (!context.chatId) { + return { success: false, error: 'No chat context available for outputs/' } + } + const filename = path.slice('outputs/'.length).split('/')[0] + const outputResult = await readChatOutput(filename, context.chatId) + if (outputResult) { + const isAttachment = hasModelAttachment(outputResult) + if ( + !isAttachment && + (isOversizedReadPlaceholder(outputResult.content) || + serializedResultSize(outputResult) > TOOL_RESULT_MAX_INLINE_CHARS) + ) { + logger.warn('Output read result too large', { + path, + hasAttachment: isAttachment, + contentLength: outputResult.content.length, + serializedSize: serializedResultSize(outputResult), + }) + return { + success: false, + error: isOversizedReadPlaceholder(outputResult.content) + ? outputResult.content + : 'Read result too large to return inline. Use grep with a more specific pattern or narrower path to locate the relevant section, then retry read with offset/limit.', + } + } + const windowedOutput = applyWindow(outputResult) + logger.debug('vfs_read resolved chat output', { + path, + totalLines: outputResult.totalLines, + hasAttachment: isAttachment, + offset, + limit, + }) + return { success: true, output: windowedOutput } + } + return { + success: false, + error: `Output not found: ${path}. Use glob("outputs/*") to list available outputs.`, + } + } + const vfs = await getOrMaterializeVFS(workspaceId, context.userId) // Plain canonical file leaves are metadata resources. Dynamic file content diff --git a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts index a83fa4a8b84..199800b0998 100644 --- a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts +++ b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.ts @@ -19,6 +19,7 @@ export function createServerToolHandler(toolId: string): ToolHandler { workspaceId: context.workspaceId, userPermission: context.userPermission ?? undefined, chatId: context.chatId, + interactive: context.interactive, messageId: context.messageId, parentToolCallId: context.parentToolCallId, abortSignal: context.abortSignal, diff --git a/apps/sim/lib/copilot/tools/server/base-tool.ts b/apps/sim/lib/copilot/tools/server/base-tool.ts index dc03ae805b2..73b29c4cd13 100644 --- a/apps/sim/lib/copilot/tools/server/base-tool.ts +++ b/apps/sim/lib/copilot/tools/server/base-tool.ts @@ -13,6 +13,14 @@ export interface ServerToolContext { * intent. Undefined for main-agent tool calls (which never overlap). */ parentToolCallId?: string + /** + * True only for genuine interactive chat turns (copilot/mothership UI), which + * always have a persisted `copilot_chats` row. False/undefined for headless + * runs (e.g. Mothership block execution) whose `chatId` is an ephemeral, + * non-persisted id. Only interactive turns may write chat-scoped `outputs/` + * files, since those carry a `chat_id` foreign key to `copilot_chats`. + */ + interactive?: boolean abortSignal?: AbortSignal /** Fires only on explicit user stop, never on passive transport disconnect. */ userStopSignal?: AbortSignal diff --git a/apps/sim/lib/copilot/tools/server/files/create-file.ts b/apps/sim/lib/copilot/tools/server/files/create-file.ts index 146d0237773..3e83e948b41 100644 --- a/apps/sim/lib/copilot/tools/server/files/create-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/create-file.ts @@ -52,6 +52,13 @@ export const createFileServerTool: BaseServerTool" path — mothership's + // chat-scoped-outputs flag steers the agent to pass one (and resource-writer + // redirects outputs/ to files/ for non-interactive runs, which lack a + // persisted copilot_chats row). const outputPath = outputFile?.path ?? `files/${fileName}` assertServerToolNotAborted(context) @@ -192,6 +197,8 @@ export const downloadToWorkspaceFileServerTool: BaseServerTool< const written = await writeWorkspaceFileByPath({ workspaceId, userId: context.userId, + chatId: context.chatId, + interactive: context.interactive, target: { path: outputPath, mode: outputFile?.mode ?? 'create', diff --git a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts index 7ed7fb69dd2..a0765d9bbed 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -146,6 +146,12 @@ export function splitWorkspaceFilePath(fileName: string): { } } +/** True when a path/name targets the chat-scoped `outputs/` namespace, which is non-editable. */ +function isOutputsPath(path: string | undefined): boolean { + if (!path) return false + return path.trim().replace(/^\/+/, '').startsWith('outputs/') +} + export interface DocumentFormatInfo { isDoc: boolean formatName?: 'PPTX' | 'DOCX' | 'PDF' @@ -290,6 +296,11 @@ export const workspaceFileServerTool: BaseServerTool" path — mothership's + // chat-scoped-outputs flag steers the agent to pass one (and resource-writer + // redirects outputs/ to files/ for non-interactive runs, which lack a + // persisted copilot_chats row). const outputPath = outputFile?.path || `files/generated-image${ext}` const imageBuffer = Buffer.from(imageBase64, 'base64') const mode = outputFile?.mode ?? 'create' @@ -170,6 +175,8 @@ export const generateImageServerTool: BaseServerTool = { } const outputFile = params.outputs?.files?.[0] + // Omitted outputs.files keeps the pre-feature `files/` default. Chat-scoped + // one-offs are opt-in via an explicit "outputs/" path — mothership's + // chat-scoped-outputs flag steers the agent to pass one (and resource-writer + // redirects outputs/ to files/ for non-interactive runs, which lack a + // persisted copilot_chats row). const outputPath = outputFile?.path || `files/ffmpeg-${params.operation}.${result.ext}` const mode = outputFile?.mode ?? 'create' @@ -131,6 +136,8 @@ export const ffmpegServerTool: BaseServerTool = { const written = await writeWorkspaceFileByPath({ workspaceId, userId: context.userId, + chatId: context.chatId, + interactive: context.interactive, target: { path: outputPath, mode, mimeType: outputFile?.mimeType }, buffer: result.buffer, inferredMimeType: result.contentType || 'application/octet-stream', diff --git a/apps/sim/lib/copilot/tools/server/media/generate-audio.ts b/apps/sim/lib/copilot/tools/server/media/generate-audio.ts index 6d224686397..dcd498519d4 100644 --- a/apps/sim/lib/copilot/tools/server/media/generate-audio.ts +++ b/apps/sim/lib/copilot/tools/server/media/generate-audio.ts @@ -114,6 +114,11 @@ export const generateAudioServerTool: BaseServerTool" path — mothership's + // chat-scoped-outputs flag steers the agent to pass one (and resource-writer + // redirects outputs/ to files/ for non-interactive runs, which lack a + // persisted copilot_chats row). const outputPath = outputFile?.path || `files/generated-audio.${ext}` const mode = outputFile?.mode ?? 'create' @@ -121,6 +126,8 @@ export const generateAudioServerTool: BaseServerTool" path — mothership's + // chat-scoped-outputs flag steers the agent to pass one (and resource-writer + // redirects outputs/ to files/ for non-interactive runs, which lack a + // persisted copilot_chats row). const outputPath = outputFile?.path || 'files/generated-video.mp4' const mode = outputFile?.mode ?? 'create' @@ -97,6 +102,8 @@ export const generateVideoServerTool: BaseServerTool { const contentType = args.target.mimeType || args.inferredMimeType + + // Chat-scoped one-off output: persisted under the `outputs/` namespace instead + // of the workspace. Outputs are flat and write-once (non-editable) — the agent + // materializes one to `files/` first if it needs to edit it. + const normalizedTargetPath = args.target.path.trim().replace(/^\/+/, '') + if (normalizedTargetPath.startsWith('outputs/')) { + // Outputs are flat (no folders), so collapse to the leaf name. + const decoded = decodeVfsPathSegments(normalizedTargetPath.slice('outputs/'.length)) + const leafName = normalizeWorkspaceFileItemName(decoded.at(-1) ?? '', 'File') + if (!leafName) { + throw new Error('outputs/ path must include a file name') + } + + // Only interactive chats have a persisted copilot_chats row to satisfy the + // chat_id FK. Otherwise redirect to a persisted files/ write below. + if (args.chatId && args.interactive) { + if (args.target.mode === 'overwrite') { + throw new Error( + 'outputs/ files are write-once and cannot be overwritten. Generate a new output, or materialize it to files/ to edit.' + ) + } + const uploaded = await uploadChatOutput({ + workspaceId: args.workspaceId, + userId: args.userId, + chatId: args.chatId, + fileBuffer: args.buffer, + fileName: leafName, + contentType, + }) + return { + id: uploaded.id, + name: uploaded.name, + size: uploaded.size, + contentType: uploaded.type, + downloadUrl: uploaded.url, + vfsPath: `outputs/${encodeVfsSegment(uploaded.name)}`, + mode: 'create', + } + } + + // Non-interactive / no persisted chat: fall through to a normal workspace + // (files/) write so the asset still persists instead of hitting the FK error. + args = { + ...args, + target: { ...args.target, path: `files/${encodeVfsSegment(leafName)}` }, + } + } + const alias = await resolveWorkflowAliasForWorkspace({ workspaceId: args.workspaceId, path: args.target.path, diff --git a/apps/sim/lib/uploads/config.ts b/apps/sim/lib/uploads/config.ts index d833d19c9f8..89dbe11daf7 100644 --- a/apps/sim/lib/uploads/config.ts +++ b/apps/sim/lib/uploads/config.ts @@ -176,6 +176,7 @@ function getS3Config(context: StorageContext): StorageConfig { region: S3_EXECUTION_FILES_CONFIG.region, } case 'mothership': + case 'output': case 'workspace': return { bucket: S3_CONFIG.bucket, @@ -238,6 +239,7 @@ function getBlobConfig(context: StorageContext): StorageConfig { containerName: BLOB_EXECUTION_FILES_CONFIG.containerName, } case 'mothership': + case 'output': case 'workspace': return { accountName: BLOB_CONFIG.accountName, diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 8e091d8d8f2..b8eafc247c3 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -9,7 +9,7 @@ import { workspaceFiles } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getErrorMessage, getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' -import { and, eq, isNull, sql } from 'drizzle-orm' +import { and, eq, inArray, isNull, sql } from 'drizzle-orm' import type { ShareRecord } from '@/lib/api/contracts/public-shares' import { checkStorageQuota, @@ -71,8 +71,8 @@ export interface WorkspaceFileRecord { deletedAt?: Date | null uploadedAt: Date updatedAt: Date - /** Pass-through to `downloadFile` when not default `workspace` (e.g. chat mothership uploads). */ - storageContext?: 'workspace' | 'mothership' + /** Pass-through to `downloadFile` when not default `workspace` (e.g. chat mothership uploads, agent outputs). */ + storageContext?: 'workspace' | 'mothership' | 'output' /** Public share state, attached at the API boundary. `null` when never shared. */ share?: ShareRecord | null } @@ -148,7 +148,7 @@ function withCopySuffix(fileName: string, n: number): string { /** * Picks a display name that does not collide with an active workspace file (`original_name`). */ -async function allocateUniqueWorkspaceFileName( +export async function allocateUniqueWorkspaceFileName( workspaceId: string, baseName: string, folderId?: string | null @@ -547,6 +547,131 @@ export async function trackChatUpload( throw new FileConflictError(fileName) } +const MAX_CHAT_OUTPUT_NAME_RETRIES = 1000 + +/** + * Whether an active chat-scoped output already uses this VFS name in the chat. + * Advisory only — there is no DB unique index for outputs (by design: no migration) — + * so a rare concurrent insert can still produce duplicate names, in which case the + * reader resolves the most recent. Good enough for one-off generated files. + */ +async function chatOutputNameExists(chatId: string, name: string): Promise { + const rows = await db + .select({ id: workspaceFiles.id }) + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.chatId, chatId), + eq(workspaceFiles.context, 'output'), + eq(workspaceFiles.displayName, name), + isNull(workspaceFiles.deletedAt) + ) + ) + .limit(1) + return rows.length > 0 +} + +/** Pick a collision-free `displayName` within the chat's outputs (suffixes ` (2)`, ` (3)`, ...). */ +async function allocateChatOutputName(chatId: string, baseName: string): Promise { + for (let n = 1; n <= MAX_CHAT_OUTPUT_NAME_RETRIES; n++) { + const candidate = suffixedName(baseName, n) + if (!(await chatOutputNameExists(chatId, candidate))) { + return candidate + } + } + throw new FileConflictError(baseName) +} + +/** + * Persist an agent-generated one-off file as a chat-scoped output. + * + * Mirrors {@link trackChatUpload} (the user-upload equivalent) but for files the + * AGENT generates: same workspace storage bucket and `workspace_files` table, but + * tagged `context='output'` with the owning `chatId`. The copilot VFS exposes these + * under `outputs/`; they never appear in the Files UI and are deleted + * with the chat (see CHAT_SCOPED_CONTEXTS in chat-cleanup). Outputs are write-once — + * there is no update path; to edit one the agent materializes it to `files/` first. + */ +export async function uploadChatOutput(args: { + workspaceId: string + userId: string + chatId: string + fileBuffer: Buffer + fileName: string + contentType: string +}): Promise { + const { workspaceId, userId, chatId, fileBuffer, fileName, contentType } = args + logger.info(`Uploading chat output file: ${fileName} for chat ${chatId}`) + + const normalizedFileName = normalizeWorkspaceFileItemName(fileName, 'File') + + const quotaCheck = await checkStorageQuota(userId, fileBuffer.length) + if (!quotaCheck.allowed) { + throw new Error(quotaCheck.error || 'Storage limit exceeded') + } + + const displayName = await allocateChatOutputName(chatId, normalizedFileName) + const storageKey = generateWorkspaceFileKey(workspaceId, displayName) + const fileId = `wf_${generateShortId()}` + + try { + // NOTE: do NOT pass `metadata` here. When `uploadFile` receives `metadata` it + // ALSO inserts a `workspace_files` row (via insertFileMetadataHelper) keyed on + // the storage key. We do our own chat-scoped insert below (with chat_id + + // display_name + context 'output'), so passing metadata would produce a second + // row with the same key and fail the `workspace_files_key_active_unique` index + // (duplicate key, 23505). Upload the bytes only; this insert is the sole DB row. + const uploadResult = await uploadFile({ + file: fileBuffer, + fileName: storageKey, + contentType, + context: 'output', + preserveKey: true, + customKey: storageKey, + }) + + await db.insert(workspaceFiles).values({ + id: fileId, + key: uploadResult.key, + userId, + workspaceId, + context: 'output', + chatId, + originalName: displayName, + displayName, + contentType, + size: fileBuffer.length, + }) + + try { + await incrementStorageUsage(userId, fileBuffer.length, workspaceId) + } catch (storageError) { + logger.error('Failed to update storage tracking:', storageError) + } + + const pathPrefix = getServePathPrefix() + const serveUrl = `${pathPrefix}${encodeURIComponent(uploadResult.key)}?context=output` + + logger.info(`Tracked chat output: ${displayName} for chat ${chatId}`) + + return { + id: fileId, + name: displayName, + size: fileBuffer.length, + type: contentType, + url: serveUrl, + key: uploadResult.key, + context: 'output', + } + } catch (error) { + if (error instanceof FileConflictError) { + throw error + } + logger.error(`Failed to upload chat output file ${fileName}:`, error) + throw new Error(`Failed to upload output file: ${getErrorMessage(error, 'Unknown error')}`) + } +} + /** * Check if a file with the same name already exists in workspace */ @@ -853,6 +978,43 @@ export async function getWorkspaceFile( } } +/** + * Fetch a single file record by id for PREVIEW, including the chat-scoped `output` + * context (agent-generated outputs) that never appears in the workspace Files list. + * Returns the same shape as {@link listWorkspaceFiles} so the resource panel can + * render an output that {@link getWorkspaceFile}/list would miss. Authorization + * (workspace membership) is the caller's responsibility. + * + * `mothership` chat uploads are intentionally not included here — surfacing uploads + * through this preview path is out of scope for the outputs feature (see + * outputs-vfs-followups.md #2/#7) and can be added later if wanted. + */ +export async function getPreviewableWorkspaceFile( + workspaceId: string, + fileId: string +): Promise { + try { + const [file] = await db + .select() + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.id, fileId), + eq(workspaceFiles.workspaceId, workspaceId), + inArray(workspaceFiles.context, ['workspace', 'output']), + isNull(workspaceFiles.deletedAt) + ) + ) + .limit(1) + + if (!file) return null + return mapSingleWorkspaceFileRecord(file, workspaceId) + } catch (error) { + logger.error(`Failed to get previewable workspace file ${fileId}:`, error) + return null + } +} + /** * Download workspace file content */ diff --git a/apps/sim/lib/uploads/server/metadata.ts b/apps/sim/lib/uploads/server/metadata.ts index 4a3faa58366..f2b20eb335b 100644 --- a/apps/sim/lib/uploads/server/metadata.ts +++ b/apps/sim/lib/uploads/server/metadata.ts @@ -151,17 +151,24 @@ export async function insertFileMetadataMany( } /** - * Get file metadata by key with optional context filter + * Get file metadata by key with an optional context filter. `context` may be a + * single context or a list — pass a list to match any of several contexts that + * share a storage key shape (e.g. the workspace bucket's `workspace`/`mothership`/ + * `output`). Omit it to match the key in any context. */ export async function getFileMetadataByKey( key: string, - context?: StorageContext, + context?: StorageContext | StorageContext[], options?: { includeDeleted?: boolean } ): Promise { const { includeDeleted = false } = options ?? {} const conditions = [eq(workspaceFiles.key, key)] - if (context) { + if (Array.isArray(context)) { + if (context.length > 0) { + conditions.push(inArray(workspaceFiles.context, context)) + } + } else if (context) { conditions.push(eq(workspaceFiles.context, context)) } diff --git a/apps/sim/lib/uploads/shared/types.ts b/apps/sim/lib/uploads/shared/types.ts index 8998f77cd1f..3e2598c1fe2 100644 --- a/apps/sim/lib/uploads/shared/types.ts +++ b/apps/sim/lib/uploads/shared/types.ts @@ -16,6 +16,7 @@ export type StorageContext = | 'chat' | 'copilot' | 'mothership' + | 'output' | 'execution' | 'workspace' | 'profile-pictures' From f13289207e0e61279999b8948f10d20088ea5f19 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz Date: Wed, 1 Jul 2026 19:59:34 -0700 Subject: [PATCH 02/10] feat(chat): duplicate a chat as a self-contained copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Duplicate to the chat context menu. POST /api/mothership/chats/ [chatId]/duplicate clones the chat row, all messages, and the chat-owned files (uploads + outputs) under new ids/keys, rewriting every in-transcript file reference (attachment chips, embedded serve/view URLs, context chips, file resources) so the copy survives deletion of the original. Copied bytes are quota-checked up front and counted on success — deliberately diverging from the workspace-fork precedent (see comment at the increment site). Agent-side conversation state clones best-effort via the Go fork endpoint's new whole-chat mode. Duplicating navigates into the copy, titled " (Copy)". Also contract-binds the vfs outputs route (surfaced by check:api-validation): listChatOutputsContract, storageContext enum + folderId alignment, and useChatOutputs upgraded from raw fetch to requestJson. Co-Authored-By: Claude Fable 5 --- .../chats/[chatId]/duplicate/route.test.ts | 285 ++++++++++++++++++ .../chats/[chatId]/duplicate/route.ts | 225 ++++++++++++++ .../chats/[chatId]/outputs/route.ts | 8 +- .../w/components/sidebar/sidebar.tsx | 20 +- apps/sim/hooks/queries/mothership-chats.ts | 44 +++ apps/sim/hooks/queries/workspace-files.ts | 18 +- .../sim/lib/api/contracts/mothership-chats.ts | 27 ++ apps/sim/lib/api/contracts/workspace-files.ts | 2 +- .../lib/copilot/chat/duplicate-chat-files.ts | 172 +++++++++++ .../chat/rewrite-file-references.test.ts | 121 ++++++++ .../copilot/chat/rewrite-file-references.ts | 80 +++++ .../tools/handlers/output-file-reader.ts | 1 + apps/sim/lib/posthog/events.ts | 5 + scripts/check-api-validation-contracts.ts | 4 +- 14 files changed, 998 insertions(+), 14 deletions(-) create mode 100644 apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.test.ts create mode 100644 apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts create mode 100644 apps/sim/lib/copilot/chat/duplicate-chat-files.ts create mode 100644 apps/sim/lib/copilot/chat/rewrite-file-references.test.ts create mode 100644 apps/sim/lib/copilot/chat/rewrite-file-references.ts diff --git a/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.test.ts b/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.test.ts new file mode 100644 index 00000000000..b428f8ae4b6 --- /dev/null +++ b/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.test.ts @@ -0,0 +1,285 @@ +/** + * @vitest-environment node + */ +import { copilotHttpMock, copilotHttpMockFns } from '@sim/testing' +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockTransaction, + mockSelectRows, + mockCheckStorageQuota, + mockListDuplicableChatFiles, + mockPlanChatFileCopies, + mockExecuteChatFileBlobCopies, + mockLoadCopilotChatMessages, + mockAppendCopilotChatMessages, + mockAssertActiveWorkspaceAccess, + mockFetchGo, + mockPublishStatusChanged, + mockCaptureServerEvent, +} = vi.hoisted(() => ({ + mockTransaction: vi.fn(), + mockSelectRows: vi.fn(), + mockCheckStorageQuota: vi.fn(), + mockListDuplicableChatFiles: vi.fn(), + mockPlanChatFileCopies: vi.fn(), + mockExecuteChatFileBlobCopies: vi.fn(), + mockLoadCopilotChatMessages: vi.fn(), + mockAppendCopilotChatMessages: vi.fn(), + mockAssertActiveWorkspaceAccess: vi.fn(), + mockFetchGo: vi.fn(), + mockPublishStatusChanged: vi.fn(), + mockCaptureServerEvent: vi.fn(), +})) + +vi.mock('@sim/db', () => ({ + db: { + select: () => ({ + from: () => ({ + where: () => ({ + limit: () => mockSelectRows(), + }), + }), + }), + transaction: mockTransaction, + }, +})) + +vi.mock('@sim/db/schema', () => ({ + copilotChats: { + id: 'copilotChats.id', + userId: 'copilotChats.userId', + type: 'copilotChats.type', + workspaceId: 'copilotChats.workspaceId', + title: 'copilotChats.title', + model: 'copilotChats.model', + resources: 'copilotChats.resources', + previewYaml: 'copilotChats.previewYaml', + planArtifact: 'copilotChats.planArtifact', + config: 'copilotChats.config', + }, +})) + +vi.mock('drizzle-orm', () => ({ + eq: vi.fn((field: unknown, value: unknown) => ({ type: 'eq', field, value })), +})) + +vi.mock('@/lib/copilot/request/http', () => copilotHttpMock) + +vi.mock('@/lib/billing/storage', () => ({ + checkStorageQuota: mockCheckStorageQuota, +})) + +vi.mock('@/lib/copilot/chat/duplicate-chat-files', () => ({ + listDuplicableChatFiles: mockListDuplicableChatFiles, + planChatFileCopies: mockPlanChatFileCopies, + executeChatFileBlobCopies: mockExecuteChatFileBlobCopies, +})) + +vi.mock('@/lib/copilot/chat/lifecycle', () => ({ + loadCopilotChatMessages: mockLoadCopilotChatMessages, +})) + +vi.mock('@/lib/copilot/chat/messages-store', () => ({ + appendCopilotChatMessages: mockAppendCopilotChatMessages, +})) + +vi.mock('@/lib/copilot/chat-status', () => ({ + chatPubSub: { publishStatusChanged: mockPublishStatusChanged }, +})) + +vi.mock('@/lib/copilot/request/go/fetch', () => ({ + fetchGo: mockFetchGo, +})) + +vi.mock('@/lib/copilot/server/agent-url', () => ({ + getMothershipBaseURL: vi.fn().mockResolvedValue('http://mothership.test'), + getMothershipSourceEnvHeaders: vi.fn().mockReturnValue({}), +})) + +vi.mock('@/lib/core/config/env', () => ({ env: {} })) + +vi.mock('@/lib/posthog/server', () => ({ + captureServerEvent: mockCaptureServerEvent, +})) + +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + assertActiveWorkspaceAccess: mockAssertActiveWorkspaceAccess, + isWorkspaceAccessDeniedError: () => false, +})) + +import { POST } from '@/app/api/mothership/chats/[chatId]/duplicate/route' + +const OLD_FILE_ID = 'wf_oldfile' +const NEW_FILE_ID = 'wf_newfile' + +const parentRow = { + id: 'chat-1', + userId: 'user-1', + type: 'mothership', + workspaceId: 'ws-1', + title: 'Generate Logs', + model: 'claude-opus-4-8', + resources: [{ type: 'file', id: OLD_FILE_ID, title: 'cat.png' }], + previewYaml: null, + planArtifact: null, + config: null, +} + +function makeTx() { + return { + insert: () => ({ + values: () => ({ + returning: async () => [{ id: 'row-id', workspaceId: 'ws-1' }], + }), + }), + update: () => ({ + set: vi.fn().mockReturnValue({ where: async () => undefined }), + }), + } +} + +function createRequest(chatId: string) { + return new NextRequest(`http://localhost:3000/api/mothership/chats/${chatId}/duplicate`, { + method: 'POST', + }) +} + +function makeContext(chatId: string) { + return { params: Promise.resolve({ chatId }) } +} + +describe('POST /api/mothership/chats/[chatId]/duplicate', () => { + beforeEach(() => { + vi.clearAllMocks() + copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({ + userId: 'user-1', + isAuthenticated: true, + }) + mockSelectRows.mockResolvedValue([parentRow]) + mockListDuplicableChatFiles.mockResolvedValue([]) + mockCheckStorageQuota.mockResolvedValue({ allowed: true }) + mockLoadCopilotChatMessages.mockResolvedValue([]) + mockPlanChatFileCopies.mockResolvedValue({ + idMap: new Map(), + keyMap: new Map(), + blobTasks: [], + }) + mockExecuteChatFileBlobCopies.mockResolvedValue({ copied: 0, failed: 0 }) + mockAppendCopilotChatMessages.mockResolvedValue(undefined) + mockAssertActiveWorkspaceAccess.mockResolvedValue(undefined) + mockFetchGo.mockResolvedValue({ ok: true }) + mockTransaction.mockImplementation(async (cb: (tx: unknown) => Promise) => + cb(makeTx()) + ) + }) + + it('rejects unauthenticated callers', async () => { + copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({ + userId: null, + isAuthenticated: false, + }) + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(401) + }) + + it('404s when the chat belongs to another user', async () => { + mockSelectRows.mockResolvedValue([{ ...parentRow, userId: 'someone-else' }]) + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(404) + expect(mockTransaction).not.toHaveBeenCalled() + }) + + it('404s for non-mothership chats', async () => { + mockSelectRows.mockResolvedValue([{ ...parentRow, type: 'copilot' }]) + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(404) + }) + + it('fails up front with the quota error when copied bytes would exceed the limit', async () => { + mockListDuplicableChatFiles.mockResolvedValue([{ size: 600 }, { size: 400 }]) + mockCheckStorageQuota.mockResolvedValue({ allowed: false, error: 'Storage limit exceeded' }) + + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + + expect(res.status).toBe(400) + expect(mockCheckStorageQuota).toHaveBeenCalledWith('user-1', 1000) + expect(mockTransaction).not.toHaveBeenCalled() + expect(mockExecuteChatFileBlobCopies).not.toHaveBeenCalled() + }) + + it('skips the quota check entirely for a chat with no files', async () => { + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(200) + expect(mockCheckStorageQuota).not.toHaveBeenCalled() + }) + + it('duplicates the chat: copies files, rewrites references, clones agent state', async () => { + const blobTasks = [ + { + sourceKey: 'workspace/ws-1/old-cat.png', + targetKey: 'workspace/ws-1/new-cat.png', + context: 'output', + fileName: 'cat.png', + contentType: 'image/png', + }, + ] + mockListDuplicableChatFiles.mockResolvedValue([{ size: 100 }]) + mockPlanChatFileCopies.mockResolvedValue({ + idMap: new Map([[OLD_FILE_ID, NEW_FILE_ID]]), + keyMap: new Map([['workspace/ws-1/old-cat.png', 'workspace/ws-1/new-cat.png']]), + blobTasks, + }) + mockLoadCopilotChatMessages.mockResolvedValue([ + { + id: 'msg-1', + role: 'assistant', + content: `![cat](/api/files/view/${OLD_FILE_ID})`, + timestamp: '2026-07-01T00:00:00.000Z', + }, + ]) + + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + const body = await res.json() + + expect(res.status).toBe(200) + expect(body.success).toBe(true) + expect(typeof body.id).toBe('string') + + expect(mockCheckStorageQuota).toHaveBeenCalledWith('user-1', 100) + + const appended = mockAppendCopilotChatMessages.mock.calls[0] + expect(appended[0]).toBe(body.id) + expect(appended[1][0].content).toBe(`![cat](/api/files/view/${NEW_FILE_ID})`) + + expect(mockExecuteChatFileBlobCopies).toHaveBeenCalledWith(blobTasks, { + userId: 'user-1', + workspaceId: 'ws-1', + }) + + const goCall = mockFetchGo.mock.calls[0] + expect(goCall[0]).toBe('http://mothership.test/api/chats/fork') + const goBody = JSON.parse(goCall[1].body) + expect(goBody).toEqual({ sourceChatId: 'chat-1', newChatId: body.id, userId: 'user-1' }) + expect(goBody.upToMessageId).toBeUndefined() + + expect(mockPublishStatusChanged).toHaveBeenCalledWith({ + workspaceId: 'ws-1', + chatId: body.id, + type: 'created', + }) + expect(mockCaptureServerEvent).toHaveBeenCalledWith( + 'user-1', + 'task_duplicated', + { workspace_id: 'ws-1', source_chat_id: 'chat-1' }, + { groups: { workspace: 'ws-1' } } + ) + }) + + it('still succeeds when the copilot-service clone fails (best-effort)', async () => { + mockFetchGo.mockRejectedValue(new Error('mothership unreachable')) + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(200) + }) +}) diff --git a/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts new file mode 100644 index 00000000000..f3dd529c8a0 --- /dev/null +++ b/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts @@ -0,0 +1,225 @@ +import { db } from '@sim/db' +import { copilotChats } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { duplicateMothershipChatContract } from '@/lib/api/contracts/mothership-chats' +import { parseRequest } from '@/lib/api/server' +import { checkStorageQuota } from '@/lib/billing/storage' +import { + executeChatFileBlobCopies, + listDuplicableChatFiles, + planChatFileCopies, +} from '@/lib/copilot/chat/duplicate-chat-files' +import { loadCopilotChatMessages } from '@/lib/copilot/chat/lifecycle' +import { appendCopilotChatMessages } from '@/lib/copilot/chat/messages-store' +import { + rewriteMessageFileRefs, + rewriteResourceFileRefs, +} from '@/lib/copilot/chat/rewrite-file-references' +import { chatPubSub } from '@/lib/copilot/chat-status' +import { fetchGo } from '@/lib/copilot/request/go/fetch' +import { + authenticateCopilotRequestSessionOnly, + createBadRequestResponse, + createForbiddenResponse, + createInternalServerErrorResponse, + createNotFoundResponse, + createUnauthorizedResponse, +} from '@/lib/copilot/request/http' +import type { MothershipResource } from '@/lib/copilot/resources/types' +import { getMothershipBaseURL, getMothershipSourceEnvHeaders } from '@/lib/copilot/server/agent-url' +import { env } from '@/lib/core/config/env' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { captureServerEvent } from '@/lib/posthog/server' +import { + assertActiveWorkspaceAccess, + isWorkspaceAccessDeniedError, +} from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('DuplicateChatAPI') + +/** + * POST /api/mothership/chats/[chatId]/duplicate + * Creates a self-contained copy of the whole chat: row, messages, resources, + * and the chat-owned files (uploads + outputs), with every in-transcript file + * reference re-pointed at the copies. The copilot-service conversation state + * (working memory + memory files) is cloned best-effort via the fork endpoint + * in whole-chat mode (no upToMessageId). + */ +export const POST = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ chatId: string }> }) => { + try { + const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() + if (!isAuthenticated || !userId) { + return createUnauthorizedResponse() + } + + const parsed = await parseRequest(duplicateMothershipChatContract, request, context) + if (!parsed.success) return parsed.response + const { chatId } = parsed.data.params + + const [parent] = await db + .select({ + id: copilotChats.id, + userId: copilotChats.userId, + type: copilotChats.type, + workspaceId: copilotChats.workspaceId, + title: copilotChats.title, + model: copilotChats.model, + resources: copilotChats.resources, + previewYaml: copilotChats.previewYaml, + planArtifact: copilotChats.planArtifact, + config: copilotChats.config, + }) + .from(copilotChats) + .where(eq(copilotChats.id, chatId)) + .limit(1) + + if (!parent || parent.userId !== userId || parent.type !== 'mothership') { + return createNotFoundResponse('Chat not found') + } + + if (parent.workspaceId) { + await assertActiveWorkspaceAccess(parent.workspaceId, userId) + } + + const sourceFiles = await listDuplicableChatFiles(db, chatId) + const totalFileBytes = sourceFiles.reduce((sum, row) => sum + row.size, 0) + if (totalFileBytes > 0) { + const quotaCheck = await checkStorageQuota(userId, totalFileBytes) + if (!quotaCheck.allowed) { + return createBadRequestResponse(quotaCheck.error || 'Storage limit exceeded') + } + } + + const messages = await loadCopilotChatMessages(chatId) + + const parentResources = Array.isArray(parent.resources) + ? (parent.resources as MothershipResource[]) + : [] + + const newId = generateId() + const title = `${parent.title ?? 'New chat'} (Copy)` + const now = new Date() + + const result = await db.transaction(async (tx) => { + const [row] = await tx + .insert(copilotChats) + .values({ + id: newId, + userId, + workspaceId: parent.workspaceId, + type: parent.type, + title, + model: parent.model, + resources: parentResources, + previewYaml: parent.previewYaml, + planArtifact: parent.planArtifact, + config: parent.config, + conversationId: null, + updatedAt: now, + lastSeenAt: now, + }) + .returning({ id: copilotChats.id, workspaceId: copilotChats.workspaceId }) + + if (!row) return null + + // File rows FK the new chat row, so the plan runs after the insert. + const { idMap, keyMap, blobTasks } = await planChatFileCopies({ + tx, + rows: sourceFiles, + newChatId: newId, + userId, + now, + }) + + const maps = { fileIds: idMap, fileKeys: keyMap } + if (idMap.size > 0 || keyMap.size > 0) { + await tx + .update(copilotChats) + .set({ resources: rewriteResourceFileRefs(parentResources, maps) }) + .where(eq(copilotChats.id, newId)) + } + + await appendCopilotChatMessages( + newId, + rewriteMessageFileRefs(messages, maps), + { chatModel: parent.model }, + tx + ) + return { row, blobTasks } + }) + + if (!result) { + return createInternalServerErrorResponse('Failed to create duplicated chat') + } + + const { copied, failed } = await executeChatFileBlobCopies(result.blobTasks, { + userId, + workspaceId: parent.workspaceId ?? undefined, + }) + if (failed > 0) { + logger.warn('Some chat file blobs failed to copy during duplicate', { + chatId, + newChatId: newId, + copied, + failed, + }) + } + + // Clone copilot-service conversation state (messages, active window, memory + // files) in whole-chat mode. Best-effort: if the copilot service doesn't + // have a row for the source chat yet (no messages sent), skip. + try { + const copilotHeaders: Record = { 'Content-Type': 'application/json' } + if (env.COPILOT_API_KEY) { + copilotHeaders['x-api-key'] = env.COPILOT_API_KEY + } + Object.assign(copilotHeaders, getMothershipSourceEnvHeaders()) + const mothershipBaseURL = await getMothershipBaseURL({ userId }) + const copilotRes = await fetchGo(`${mothershipBaseURL}/api/chats/fork`, { + method: 'POST', + headers: copilotHeaders, + body: JSON.stringify({ + sourceChatId: chatId, + newChatId: newId, + userId, + }), + spanName: 'sim → go /api/chats/fork', + operation: 'duplicate_chat', + }) + if (!copilotRes.ok) { + const text = await copilotRes.text().catch(() => '') + logger.warn('Copilot duplicate returned non-OK', { status: copilotRes.status, body: text }) + } + } catch (err) { + logger.warn('Failed to duplicate copilot-service conversation, skipping', { err }) + } + + if (result.row.workspaceId) { + chatPubSub?.publishStatusChanged({ + workspaceId: result.row.workspaceId, + chatId: newId, + type: 'created', + }) + } + + captureServerEvent( + userId, + 'task_duplicated', + { workspace_id: parent.workspaceId ?? '', source_chat_id: chatId }, + { groups: { workspace: parent.workspaceId ?? '' } } + ) + + return NextResponse.json({ success: true, id: newId }) + } catch (error) { + if (isWorkspaceAccessDeniedError(error)) { + return createForbiddenResponse('Workspace access denied') + } + logger.error('Error duplicating chat:', error) + return createInternalServerErrorResponse('Failed to duplicate chat') + } + } +) diff --git a/apps/sim/app/api/mothership/chats/[chatId]/outputs/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/outputs/route.ts index 3f05fa4fad7..defd261a562 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/outputs/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/outputs/route.ts @@ -1,6 +1,8 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' +import { listChatOutputsContract } from '@/lib/api/contracts/mothership-chats' +import { parseRequest } from '@/lib/api/server' import { getAccessibleCopilotChatAuth } from '@/lib/copilot/chat/lifecycle' import { authenticateCopilotRequestSessionOnly, @@ -21,14 +23,16 @@ const logger = createLogger('MothershipChatOutputsAPI') * shape as the workspace file list. */ export const GET = withRouteHandler( - async (_request: NextRequest, context: { params: Promise<{ chatId: string }> }) => { + async (request: NextRequest, context: { params: Promise<{ chatId: string }> }) => { try { const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() if (!isAuthenticated || !userId) { return createUnauthorizedResponse() } - const { chatId } = await context.params + const parsed = await parseRequest(listChatOutputsContract, request, context) + if (!parsed.success) return parsed.response + const { chatId } = parsed.data.params const chat = await getAccessibleCopilotChatAuth(chatId, userId) if (!chat) { return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 2e94f37a861..7143a5f27ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -92,6 +92,7 @@ import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' import { useDeleteMothershipChat, useDeleteMothershipChats, + useDuplicateMothershipChat, useMarkMothershipChatRead, useMarkMothershipChatUnread, useMothershipChats, @@ -597,6 +598,7 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { const deleteChatMutation = useDeleteMothershipChat(workspaceId) const deleteChatsMutation = useDeleteMothershipChats(workspaceId) + const duplicateChatMutation = useDuplicateMothershipChat(workspaceId) const markChatReadMutation = useMarkMothershipChatRead(workspaceId) const markChatUnreadMutation = useMarkMothershipChatUnread(workspaceId) const renameChatMutation = useRenameMothershipChat(workspaceId) @@ -963,6 +965,20 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { chatFlyoutRename.startRename({ id: chatId, name: chat.name }) }, [chatFlyoutRename, chats, chatsHover]) + const handleDuplicateChat = useCallback(() => { + const { chatIds: ids } = contextMenuSelectionRef.current + if (ids.length !== 1) return + duplicateChatMutation.mutate( + { chatId: ids[0] }, + { + onSuccess: (result) => { + useFolderStore.getState().clearChatSelection() + navigateToPage(`/workspace/${workspaceId}/chat/${result.id}`) + }, + } + ) + }, [navigateToPage, workspaceId]) + const handleToggleChatPin = useCallback(() => { const { chatIds: ids } = contextMenuSelectionRef.current if (ids.length !== 1) return @@ -1686,6 +1702,7 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { onMarkAsUnread={handleMarkChatAsUnread} onTogglePin={handleToggleChatPin} onRename={handleStartChatRename} + onDuplicate={handleDuplicateChat} onDelete={handleDeleteChat} showOpenInNewTab={!isMultiChatContextMenu} showMarkAsRead={!isMultiChatContextMenu && !!activeChatContextMenuItem?.isUnread} @@ -1697,8 +1714,9 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { showPin={!isMultiChatContextMenu && !!activeChatContextMenuItem} isPinned={!!activeChatContextMenuItem?.isPinned} showRename={!isMultiChatContextMenu} - showDuplicate={false} + showDuplicate={!isMultiChatContextMenu} disableRename={!canEdit} + disableDuplicate={!canEdit || duplicateChatMutation.isPending} disableDelete={!canEdit} /> diff --git a/apps/sim/hooks/queries/mothership-chats.ts b/apps/sim/hooks/queries/mothership-chats.ts index 10e458c5aef..ec21cf94cfd 100644 --- a/apps/sim/hooks/queries/mothership-chats.ts +++ b/apps/sim/hooks/queries/mothership-chats.ts @@ -12,6 +12,7 @@ import { addMothershipChatResourceContract, createMothershipChatContract, deleteMothershipChatContract, + duplicateMothershipChatContract, forkMothershipChatContract, getMothershipChatContract, listMothershipChatsContract, @@ -718,3 +719,46 @@ export function useForkMothershipChat(workspaceId?: string) { }, }) } + +async function duplicateChat(params: { chatId: string }): Promise<{ id: string }> { + const data = await requestJson(duplicateMothershipChatContract, { + params: { chatId: params.chatId }, + }) + return { id: data.id } +} + +export function useDuplicateMothershipChat(workspaceId?: string) { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: duplicateChat, + onSuccess: async (data, variables) => { + if (!workspaceId) return + await queryClient.cancelQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) + const existing = queryClient.getQueryData( + mothershipChatKeys.list(workspaceId) + ) + if (existing) { + const sourceChat = existing.find((t) => t.id === variables.chatId) + const duplicateEntry: MothershipChatMetadata = { + id: data.id, + name: `${sourceChat?.name ?? 'New chat'} (Copy)`, + updatedAt: new Date(), + isActive: false, + isUnread: false, + isPinned: false, + } + const pinnedCount = existing.findIndex((chat) => !chat.isPinned) + const insertAt = pinnedCount === -1 ? existing.length : pinnedCount + queryClient.setQueryData(mothershipChatKeys.list(workspaceId), [ + ...existing.slice(0, insertAt), + duplicateEntry, + ...existing.slice(insertAt), + ]) + } + }, + onSettled: () => { + if (!workspaceId) return + queryClient.invalidateQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) + }, + }) +} diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index a426caaef81..aeb1d89d87e 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -6,6 +6,7 @@ import { backoffWithJitter } from '@sim/utils/retry' import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { ApiClientError, isApiClientError } from '@/lib/api/client/errors' import { requestJson } from '@/lib/api/client/request' +import { listChatOutputsContract } from '@/lib/api/contracts/mothership-chats' import { getUsageLimitsContract } from '@/lib/api/contracts/usage-limits' import { deleteWorkspaceFileContract, @@ -94,14 +95,15 @@ export function useChatOutputs(chatId: string | undefined) { queryKey: workspaceFilesKeys.chatOutputs(chatId ?? ''), queryFn: async ({ signal }): Promise => { if (!chatId) return [] - // boundary-raw-fetch: chat-scoped outputs list; no contract - const response = await fetch( - `/api/mothership/chats/${encodeURIComponent(chatId)}/outputs`, - { signal, cache: 'no-store' } - ) - if (!response.ok) return [] - const data = await response.json().catch(() => null) - return data?.success && Array.isArray(data.files) ? (data.files as WorkspaceFileRecord[]) : [] + try { + const data = await requestJson(listChatOutputsContract, { + params: { chatId }, + signal, + }) + return data.files + } catch { + return [] + } }, enabled: !!chatId, staleTime: 30 * 1000, diff --git a/apps/sim/lib/api/contracts/mothership-chats.ts b/apps/sim/lib/api/contracts/mothership-chats.ts index 0165c512bd3..fb567359030 100644 --- a/apps/sim/lib/api/contracts/mothership-chats.ts +++ b/apps/sim/lib/api/contracts/mothership-chats.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import { scheduleContextSchema } from '@/lib/api/contracts/schedules' import { defineRouteContract } from '@/lib/api/contracts/types' +import { workspaceFileRecordSchema } from '@/lib/api/contracts/workspace-files' const dateStringSchema = z.string().refine((value) => !Number.isNaN(Date.parse(value)), { message: 'Expected a valid date string', @@ -280,6 +281,32 @@ export const forkMothershipChatContract = defineRouteContract({ }, }) +export const listChatOutputsContract = defineRouteContract({ + method: 'GET', + path: '/api/mothership/chats/[chatId]/outputs', + params: mothershipChatParamsSchema, + response: { + mode: 'json', + schema: z.object({ + success: z.literal(true), + files: z.array(workspaceFileRecordSchema), + }), + }, +}) + +export const duplicateMothershipChatContract = defineRouteContract({ + method: 'POST', + path: '/api/mothership/chats/[chatId]/duplicate', + params: mothershipChatParamsSchema, + response: { + mode: 'json', + schema: z.object({ + success: z.literal(true), + id: z.string(), + }), + }, +}) + export const createMothershipChatResponseSchema = z.object({ success: z.literal(true), id: z.string(), diff --git a/apps/sim/lib/api/contracts/workspace-files.ts b/apps/sim/lib/api/contracts/workspace-files.ts index b47b5710528..fdf933e96f2 100644 --- a/apps/sim/lib/api/contracts/workspace-files.ts +++ b/apps/sim/lib/api/contracts/workspace-files.ts @@ -65,7 +65,7 @@ export const workspaceFileRecordSchema = z.object({ deletedAt: z.coerce.date().nullable().optional(), uploadedAt: z.coerce.date(), updatedAt: z.coerce.date(), - storageContext: z.enum(['workspace', 'mothership']).optional(), + storageContext: z.enum(['workspace', 'mothership', 'output']).optional(), share: shareRecordSchema.nullable().optional(), }) diff --git a/apps/sim/lib/copilot/chat/duplicate-chat-files.ts b/apps/sim/lib/copilot/chat/duplicate-chat-files.ts new file mode 100644 index 00000000000..2ba2d8ad9a2 --- /dev/null +++ b/apps/sim/lib/copilot/chat/duplicate-chat-files.ts @@ -0,0 +1,172 @@ +import { workspaceFiles } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { generateShortId } from '@sim/utils/id' +import { and, eq, inArray, isNull } from 'drizzle-orm' +import { incrementStorageUsage } from '@/lib/billing/storage' +import type { DbOrTx } from '@/lib/db/types' +import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { downloadFile, uploadFile } from '@/lib/uploads/core/storage-service' +import type { StorageContext } from '@/lib/uploads/shared/types' +import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' + +const logger = createLogger('DuplicateChatFiles') + +/** + * The chat-owned storage contexts a duplicate must copy: user uploads + * (`mothership`) and agent-generated outputs (`output`). Shared workspace + * `files/` (`context='workspace'`) is workspace-owned, not chat-owned — both + * chats reference it in place and it is never copied. + */ +export const DUPLICABLE_CHAT_FILE_CONTEXTS: readonly StorageContext[] = ['mothership', 'output'] + +export type DuplicableChatFileRow = typeof workspaceFiles.$inferSelect + +/** One blob byte-copy to run after the duplicate transaction commits. */ +export interface ChatBlobCopyTask { + sourceKey: string + targetKey: string + context: StorageContext + fileName: string + contentType: string +} + +export interface PlanChatFileCopiesResult { + /** source `workspace_files.id` -> copy id (rewrites view-URLs, attachment ids, resource ids). */ + idMap: Map + /** source storage key -> copy storage key (rewrites serve-URLs, attachment keys). */ + keyMap: Map + /** Blob duplications to run after the transaction commits. */ + blobTasks: ChatBlobCopyTask[] +} + +/** + * The live chat-scoped file rows a duplicate copies: uploads + outputs owned by + * the chat, excluding soft-deleted rows. Also used pre-transaction to sum sizes + * for the storage-quota gate. + */ +export async function listDuplicableChatFiles( + db: DbOrTx, + chatId: string +): Promise { + return db + .select() + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.chatId, chatId), + inArray(workspaceFiles.context, [...DUPLICABLE_CHAT_FILE_CONTEXTS]), + isNull(workspaceFiles.deletedAt) + ) + ) +} + +/** + * Insert copy rows for the source chat's files under the new chat id (fresh + * `wf_` id + fresh storage key; display names carry over verbatim because their + * uniqueness is per-chat and the new chat is an empty namespace). Returns the + * old->new id/key maps that drive the reference rewrite, plus the blob + * byte-copies to run post-commit. Runs inside the duplicate transaction so a + * failed insert rolls the whole duplicate back; blob I/O is deferred to + * {@link executeChatFileBlobCopies}. Modeled on the workspace-fork copy + * (`lib/workspaces/fork/copy/copy-files.ts`), adapted for chat-scoped rows. + */ +export async function planChatFileCopies(params: { + tx: DbOrTx + rows: DuplicableChatFileRow[] + newChatId: string + userId: string + now: Date +}): Promise { + const { tx, rows, newChatId, userId, now } = params + const idMap = new Map() + const keyMap = new Map() + const blobTasks: ChatBlobCopyTask[] = [] + + for (const row of rows) { + if (!row.workspaceId) { + logger.warn('Skipping chat file with no workspaceId during duplicate', { fileId: row.id }) + continue + } + const copyId = `wf_${generateShortId()}` + const targetKey = generateWorkspaceFileKey(row.workspaceId, row.originalName) + await tx.insert(workspaceFiles).values({ + ...row, + id: copyId, + key: targetKey, + chatId: newChatId, + userId, + deletedAt: null, + uploadedAt: now, + updatedAt: now, + }) + idMap.set(row.id, copyId) + keyMap.set(row.key, targetKey) + blobTasks.push({ + sourceKey: row.key, + targetKey, + context: row.context as StorageContext, + fileName: row.originalName, + contentType: row.contentType, + }) + } + + return { idMap, keyMap, blobTasks } +} + +/** + * Copy each planned blob to its new key, best-effort: a failed copy logs a + * warning and is skipped (the duplicate keeps its transcript; that one file is + * missing) rather than failing the whole duplicate. Each successfully copied + * file increments the storage-usage counter by its actual byte length. + */ +export async function executeChatFileBlobCopies( + blobTasks: ChatBlobCopyTask[], + params: { userId: string; workspaceId?: string } +): Promise<{ copied: number; failed: number }> { + let copied = 0 + let failed = 0 + for (const task of blobTasks) { + try { + const buffer = await downloadFile({ + key: task.sourceKey, + context: task.context, + maxBytes: MAX_FILE_SIZE, + }) + // No `metadata` here on purpose: passing it would make uploadFile insert + // its own workspace_files row (without chatId), colliding with the row + // the transaction already created for this key. + await uploadFile({ + file: buffer, + fileName: task.fileName, + contentType: task.contentType, + context: task.context, + customKey: task.targetKey, + preserveKey: true, + }) + copied += 1 + try { + // Duplicated bytes COUNT against the storage quota, deliberately + // diverging from the workspace-fork copy path + // (lib/workspaces/fork/copy/copy-files.ts), which copies blobs without + // counting them. A chat duplicate stores a second physical copy of + // every upload/output, so the counter must reflect it. Do not "fix" + // this back to the fork precedent. + await incrementStorageUsage(params.userId, buffer.length, params.workspaceId) + } catch (error) { + logger.error('Failed to update storage tracking for duplicated chat file', { + targetKey: task.targetKey, + error: getErrorMessage(error), + }) + } + } catch (error) { + failed += 1 + logger.warn('Failed to copy chat file blob during duplicate', { + sourceKey: task.sourceKey, + targetKey: task.targetKey, + error: getErrorMessage(error), + }) + } + } + return { copied, failed } +} diff --git a/apps/sim/lib/copilot/chat/rewrite-file-references.test.ts b/apps/sim/lib/copilot/chat/rewrite-file-references.test.ts new file mode 100644 index 00000000000..deef947f8b1 --- /dev/null +++ b/apps/sim/lib/copilot/chat/rewrite-file-references.test.ts @@ -0,0 +1,121 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { PersistedMessage } from '@/lib/copilot/chat/persisted-message' +import { + type ChatFileRefMaps, + rewriteMessageFileRefs, + rewriteResourceFileRefs, +} from '@/lib/copilot/chat/rewrite-file-references' +import type { MothershipResource } from '@/lib/copilot/resources/types' + +const OLD_ID = 'wf_oldfileid123' +const NEW_ID = 'wf_newfileid456' +const OLD_KEY = 'workspace/ws-1/1719000000-aabb-cat.png' +const NEW_KEY = 'workspace/ws-1/1720000000-ccdd-cat.png' + +const maps: ChatFileRefMaps = { + fileIds: new Map([[OLD_ID, NEW_ID]]), + fileKeys: new Map([[OLD_KEY, NEW_KEY]]), +} + +const emptyMaps: ChatFileRefMaps = { fileIds: new Map(), fileKeys: new Map() } + +function makeMessage(overrides: Partial): PersistedMessage { + return { + id: 'msg-1', + role: 'assistant', + content: '', + timestamp: '2026-07-01T00:00:00.000Z', + ...overrides, + } +} + +describe('rewriteMessageFileRefs', () => { + it('rewrites serve-URL (encoded key), view-URL, and sim:file refs in content', () => { + const content = [ + `![cat](/api/files/serve/${encodeURIComponent(OLD_KEY)})`, + `[open](/api/files/view/${OLD_ID})`, + `see sim:file/${OLD_ID}`, + ].join('\n') + const [result] = rewriteMessageFileRefs([makeMessage({ content })], maps) + + expect(result.content).toContain(`/api/files/serve/${encodeURIComponent(NEW_KEY)}`) + expect(result.content).toContain(`/api/files/view/${NEW_ID}`) + expect(result.content).toContain(`sim:file/${NEW_ID}`) + expect(result.content).not.toContain(OLD_ID) + }) + + it('rewrites text content blocks', () => { + const [result] = rewriteMessageFileRefs( + [ + makeMessage({ + contentBlocks: [{ type: 'text', content: `![x](/api/files/view/${OLD_ID})` }], + }), + ], + maps + ) + expect(result.contentBlocks?.[0].content).toBe(`![x](/api/files/view/${NEW_ID})`) + }) + + it('rewrites attachment chip id and key', () => { + const [result] = rewriteMessageFileRefs( + [ + makeMessage({ + fileAttachments: [ + { id: OLD_ID, key: OLD_KEY, filename: 'cat.png', media_type: 'image/png', size: 10 }, + ], + }), + ], + maps + ) + expect(result.fileAttachments?.[0]).toMatchObject({ id: NEW_ID, key: NEW_KEY }) + }) + + it('rewrites context chip fileId but leaves non-file contexts untouched', () => { + const [result] = rewriteMessageFileRefs( + [ + makeMessage({ + contexts: [ + { kind: 'file', label: 'cat.png', fileId: OLD_ID }, + { kind: 'workflow', label: 'My flow', workflowId: 'wflow-1' }, + ], + }), + ], + maps + ) + expect(result.contexts?.[0].fileId).toBe(NEW_ID) + expect(result.contexts?.[1]).toEqual({ kind: 'workflow', label: 'My flow', workflowId: 'wflow-1' }) + }) + + it('leaves unmapped references unchanged (graceful broken link, never corrupted)', () => { + const content = '/api/files/view/wf_someotherfile' + const [result] = rewriteMessageFileRefs([makeMessage({ content })], maps) + expect(result.content).toBe(content) + }) + + it('returns the input array identity when there is nothing to rewrite', () => { + const messages = [makeMessage({ content: `see /api/files/view/${OLD_ID}` })] + expect(rewriteMessageFileRefs(messages, emptyMaps)).toBe(messages) + }) +}) + +describe('rewriteResourceFileRefs', () => { + it('rewrites file resources and passes every other type through', () => { + const resources: MothershipResource[] = [ + { type: 'file', id: OLD_ID, title: 'cat.png' }, + { type: 'workflow', id: 'wflow-1', title: 'My flow' }, + { type: 'file', id: 'wf_unmapped', title: 'other.png' }, + ] + const result = rewriteResourceFileRefs(resources, maps) + expect(result[0].id).toBe(NEW_ID) + expect(result[1]).toEqual(resources[1]) + expect(result[2].id).toBe('wf_unmapped') + }) + + it('returns the input array identity when maps are empty', () => { + const resources: MothershipResource[] = [{ type: 'file', id: OLD_ID, title: 'cat.png' }] + expect(rewriteResourceFileRefs(resources, emptyMaps)).toBe(resources) + }) +}) diff --git a/apps/sim/lib/copilot/chat/rewrite-file-references.ts b/apps/sim/lib/copilot/chat/rewrite-file-references.ts new file mode 100644 index 00000000000..78882fe973a --- /dev/null +++ b/apps/sim/lib/copilot/chat/rewrite-file-references.ts @@ -0,0 +1,80 @@ +import type { PersistedMessage } from '@/lib/copilot/chat/persisted-message' +import type { MothershipResource } from '@/lib/copilot/resources/types' +import { rewriteForkContentRefs } from '@/lib/workspaces/fork/remap/remap-content-refs' + +/** + * Old->new translation tables produced while copying a chat's files + * (`planChatFileCopies`): row ids (view-URLs, attachment ids, resource ids, + * context chips) and storage keys (serve-URLs, attachment keys). + */ +export interface ChatFileRefMaps { + fileIds: ReadonlyMap + fileKeys: ReadonlyMap +} + +function hasMappings(maps: ChatFileRefMaps): boolean { + return maps.fileIds.size > 0 || maps.fileKeys.size > 0 +} + +function rewriteText(text: string, maps: ChatFileRefMaps): string { + return rewriteForkContentRefs(text, { fileIds: maps.fileIds, fileKeys: maps.fileKeys }) +} + +/** + * Re-point every file reference in a copied transcript at the copied files, so + * the duplicate is self-contained (it survives the original chat's deletion). + * Rewrites: free-text URLs in `content` and text content blocks (serve/view/ + * in-app/`sim:file` forms, via the shared fork grammar), attachment chip + * ids+keys, and `@`-mention context chip file ids. References to anything not + * in the maps (shared workspace files, workflows, other chats) pass through + * unchanged. Pure; returns the input array untouched when there is nothing to + * rewrite. + */ +export function rewriteMessageFileRefs( + messages: PersistedMessage[], + maps: ChatFileRefMaps +): PersistedMessage[] { + if (!hasMappings(maps)) return messages + return messages.map((message) => { + const rewritten: PersistedMessage = { + ...message, + content: rewriteText(message.content, maps), + } + if (message.contentBlocks?.length) { + rewritten.contentBlocks = message.contentBlocks.map((block) => + block.content ? { ...block, content: rewriteText(block.content, maps) } : block + ) + } + if (message.fileAttachments?.length) { + rewritten.fileAttachments = message.fileAttachments.map((att) => ({ + ...att, + id: maps.fileIds.get(att.id) ?? att.id, + key: maps.fileKeys.get(att.key) ?? att.key, + })) + } + if (message.contexts?.length) { + rewritten.contexts = message.contexts.map((ctx) => + ctx.fileId ? { ...ctx, fileId: maps.fileIds.get(ctx.fileId) ?? ctx.fileId } : ctx + ) + } + return rewritten + }) +} + +/** + * Re-point `file`-typed resource entries (the chat's attached-resources list + * stores raw `workspace_files.id`s) at the copied files. Non-file resources + * (workflows, tables, knowledge bases…) reference shared workspace entities + * and pass through unchanged. + */ +export function rewriteResourceFileRefs( + resources: MothershipResource[], + maps: ChatFileRefMaps +): MothershipResource[] { + if (!hasMappings(maps)) return resources + return resources.map((resource) => + resource.type === 'file' + ? { ...resource, id: maps.fileIds.get(resource.id) ?? resource.id } + : resource + ) +} diff --git a/apps/sim/lib/copilot/tools/handlers/output-file-reader.ts b/apps/sim/lib/copilot/tools/handlers/output-file-reader.ts index 77fdea16706..dfd8ae9e44f 100644 --- a/apps/sim/lib/copilot/tools/handlers/output-file-reader.ts +++ b/apps/sim/lib/copilot/tools/handlers/output-file-reader.ts @@ -58,6 +58,7 @@ function toWorkspaceFileRecord(row: typeof workspaceFiles.$inferSelect): Workspa size: row.size, type: row.contentType, uploadedBy: row.userId, + folderId: null, deletedAt: row.deletedAt, uploadedAt: row.uploadedAt, updatedAt: row.updatedAt, diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index a408325000e..e2246e80305 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -494,6 +494,11 @@ export interface PostHogEventMap { source_chat_id: string } + task_duplicated: { + workspace_id: string + source_chat_id: string + } + task_marked_unread: { workspace_id: string } diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 0d0d94230aa..9f5d387d0a1 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 882, - zodRoutes: 882, + totalRoutes: 884, + zodRoutes: 884, nonZodRoutes: 0, } as const From 8f99fba9be7df3be2c2a1edf9a3989f547ba8b5d Mon Sep 17 00:00:00 2001 From: Justin Blumencranz Date: Thu, 2 Jul 2026 13:03:04 -0700 Subject: [PATCH 03/10] test(vfs): track generalized outputs/ grep message; biome formatting --- .../app/api/mothership/chats/[chatId]/duplicate/route.ts | 5 ++++- apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts | 2 +- apps/sim/lib/copilot/chat/rewrite-file-references.test.ts | 6 +++++- apps/sim/lib/copilot/tools/handlers/vfs.test.ts | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts index f3dd529c8a0..9ef9b510e51 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts @@ -192,7 +192,10 @@ export const POST = withRouteHandler( }) if (!copilotRes.ok) { const text = await copilotRes.text().catch(() => '') - logger.warn('Copilot duplicate returned non-OK', { status: copilotRes.status, body: text }) + logger.warn('Copilot duplicate returned non-OK', { + status: copilotRes.status, + body: text, + }) } } catch (err) { logger.warn('Failed to duplicate copilot-service conversation, skipping', { err }) diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts index 3f5e921ab5d..62e337048f2 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts @@ -10,11 +10,11 @@ import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' +import { getPreviewableWorkspaceFile } from '@/lib/uploads/contexts/workspace' import { performDeleteWorkspaceFileItems, performRenameWorkspaceFile, } from '@/lib/workspace-files/orchestration' -import { getPreviewableWorkspaceFile } from '@/lib/uploads/contexts/workspace' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' export const dynamic = 'force-dynamic' diff --git a/apps/sim/lib/copilot/chat/rewrite-file-references.test.ts b/apps/sim/lib/copilot/chat/rewrite-file-references.test.ts index deef947f8b1..ad8e148102d 100644 --- a/apps/sim/lib/copilot/chat/rewrite-file-references.test.ts +++ b/apps/sim/lib/copilot/chat/rewrite-file-references.test.ts @@ -86,7 +86,11 @@ describe('rewriteMessageFileRefs', () => { maps ) expect(result.contexts?.[0].fileId).toBe(NEW_ID) - expect(result.contexts?.[1]).toEqual({ kind: 'workflow', label: 'My flow', workflowId: 'wflow-1' }) + expect(result.contexts?.[1]).toEqual({ + kind: 'workflow', + label: 'My flow', + workflowId: 'wflow-1', + }) }) it('leaves unmapped references unchanged (graceful broken link, never corrupted)', () => { diff --git a/apps/sim/lib/copilot/tools/handlers/vfs.test.ts b/apps/sim/lib/copilot/tools/handlers/vfs.test.ts index 72eea0cefb9..48c7c7fc6e0 100644 --- a/apps/sim/lib/copilot/tools/handlers/vfs.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/vfs.test.ts @@ -341,7 +341,7 @@ describe('vfs uploads are opt-in (like recently-deleted/)', () => { const result = await executeVfsGrep({ pattern: 'x', path: 'uploads/' }, GREP_CTX_CHAT) expect(result.success).toBe(false) - expect(result.error).toContain('single upload') + expect(result.error).toContain('single file') expect(grepChatUpload).not.toHaveBeenCalled() }) From c52b06e39d42ca9feb3c66754de4e14b172db5d5 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz Date: Thu, 2 Jul 2026 18:42:01 -0700 Subject: [PATCH 04/10] feat(chat): fork a chat from any message, replacing whole-chat duplicate The sidebar Duplicate action is replaced by a Fork button on each assistant reply (next to feedback). Forking copies the conversation up to and including the clicked message, plus the chat's uploads born at-or-before that point: each copy gets a fresh row id and storage key, the same message_id, physically copied bytes counted against the storage quota, and every in-transcript file reference re-pointed at the copies. Agent outputs/ stay behind. - workspace_files gains a nullable message_id provenance column (drizzle migration 0254); trackChatUpload stamps it from the sending user message - fork route gains the quota gate + file copy + reference rewrite, reusing the machinery built for duplicate (fork-chat-files.ts, rewrite helper) - materialize_file nulls message_id alongside chatId - duplicate route/contract/hook/tests removed; sidebar Duplicate reverted to its pre-branch disabled state (showDuplicate={false}) Co-Authored-By: Claude Fable 5 --- .../chats/[chatId]/duplicate/route.ts | 228 - .../{duplicate => fork}/route.test.ts | 103 +- .../mothership/chats/[chatId]/fork/route.ts | 73 +- .../message-actions/message-actions.tsx | 2 +- .../w/components/sidebar/sidebar.tsx | 20 +- apps/sim/hooks/queries/mothership-chats.ts | 44 - .../sim/lib/api/contracts/mothership-chats.ts | 13 - .../lib/copilot/chat/fork-chat-files.test.ts | 173 + ...icate-chat-files.ts => fork-chat-files.ts} | 86 +- apps/sim/lib/copilot/chat/payload.ts | 3 +- .../tools/handlers/materialize-file.test.ts | 45 +- .../tools/handlers/materialize-file.ts | 3 + apps/sim/lib/posthog/events.ts | 5 - .../workspace/track-chat-upload.test.ts | 45 + .../workspace/workspace-file-manager.ts | 11 +- .../migrations/0254_classy_madame_hydra.sql | 1 + .../db/migrations/meta/0254_snapshot.json | 17873 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 9 +- packages/db/schema.ts | 10 + scripts/check-api-validation-contracts.ts | 4 +- 20 files changed, 18365 insertions(+), 386 deletions(-) delete mode 100644 apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts rename apps/sim/app/api/mothership/chats/[chatId]/{duplicate => fork}/route.test.ts (72%) create mode 100644 apps/sim/lib/copilot/chat/fork-chat-files.test.ts rename apps/sim/lib/copilot/chat/{duplicate-chat-files.ts => fork-chat-files.ts} (55%) create mode 100644 packages/db/migrations/0254_classy_madame_hydra.sql create mode 100644 packages/db/migrations/meta/0254_snapshot.json diff --git a/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts deleted file mode 100644 index 9ef9b510e51..00000000000 --- a/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { db } from '@sim/db' -import { copilotChats } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { generateId } from '@sim/utils/id' -import { eq } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { duplicateMothershipChatContract } from '@/lib/api/contracts/mothership-chats' -import { parseRequest } from '@/lib/api/server' -import { checkStorageQuota } from '@/lib/billing/storage' -import { - executeChatFileBlobCopies, - listDuplicableChatFiles, - planChatFileCopies, -} from '@/lib/copilot/chat/duplicate-chat-files' -import { loadCopilotChatMessages } from '@/lib/copilot/chat/lifecycle' -import { appendCopilotChatMessages } from '@/lib/copilot/chat/messages-store' -import { - rewriteMessageFileRefs, - rewriteResourceFileRefs, -} from '@/lib/copilot/chat/rewrite-file-references' -import { chatPubSub } from '@/lib/copilot/chat-status' -import { fetchGo } from '@/lib/copilot/request/go/fetch' -import { - authenticateCopilotRequestSessionOnly, - createBadRequestResponse, - createForbiddenResponse, - createInternalServerErrorResponse, - createNotFoundResponse, - createUnauthorizedResponse, -} from '@/lib/copilot/request/http' -import type { MothershipResource } from '@/lib/copilot/resources/types' -import { getMothershipBaseURL, getMothershipSourceEnvHeaders } from '@/lib/copilot/server/agent-url' -import { env } from '@/lib/core/config/env' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { captureServerEvent } from '@/lib/posthog/server' -import { - assertActiveWorkspaceAccess, - isWorkspaceAccessDeniedError, -} from '@/lib/workspaces/permissions/utils' - -const logger = createLogger('DuplicateChatAPI') - -/** - * POST /api/mothership/chats/[chatId]/duplicate - * Creates a self-contained copy of the whole chat: row, messages, resources, - * and the chat-owned files (uploads + outputs), with every in-transcript file - * reference re-pointed at the copies. The copilot-service conversation state - * (working memory + memory files) is cloned best-effort via the fork endpoint - * in whole-chat mode (no upToMessageId). - */ -export const POST = withRouteHandler( - async (request: NextRequest, context: { params: Promise<{ chatId: string }> }) => { - try { - const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() - if (!isAuthenticated || !userId) { - return createUnauthorizedResponse() - } - - const parsed = await parseRequest(duplicateMothershipChatContract, request, context) - if (!parsed.success) return parsed.response - const { chatId } = parsed.data.params - - const [parent] = await db - .select({ - id: copilotChats.id, - userId: copilotChats.userId, - type: copilotChats.type, - workspaceId: copilotChats.workspaceId, - title: copilotChats.title, - model: copilotChats.model, - resources: copilotChats.resources, - previewYaml: copilotChats.previewYaml, - planArtifact: copilotChats.planArtifact, - config: copilotChats.config, - }) - .from(copilotChats) - .where(eq(copilotChats.id, chatId)) - .limit(1) - - if (!parent || parent.userId !== userId || parent.type !== 'mothership') { - return createNotFoundResponse('Chat not found') - } - - if (parent.workspaceId) { - await assertActiveWorkspaceAccess(parent.workspaceId, userId) - } - - const sourceFiles = await listDuplicableChatFiles(db, chatId) - const totalFileBytes = sourceFiles.reduce((sum, row) => sum + row.size, 0) - if (totalFileBytes > 0) { - const quotaCheck = await checkStorageQuota(userId, totalFileBytes) - if (!quotaCheck.allowed) { - return createBadRequestResponse(quotaCheck.error || 'Storage limit exceeded') - } - } - - const messages = await loadCopilotChatMessages(chatId) - - const parentResources = Array.isArray(parent.resources) - ? (parent.resources as MothershipResource[]) - : [] - - const newId = generateId() - const title = `${parent.title ?? 'New chat'} (Copy)` - const now = new Date() - - const result = await db.transaction(async (tx) => { - const [row] = await tx - .insert(copilotChats) - .values({ - id: newId, - userId, - workspaceId: parent.workspaceId, - type: parent.type, - title, - model: parent.model, - resources: parentResources, - previewYaml: parent.previewYaml, - planArtifact: parent.planArtifact, - config: parent.config, - conversationId: null, - updatedAt: now, - lastSeenAt: now, - }) - .returning({ id: copilotChats.id, workspaceId: copilotChats.workspaceId }) - - if (!row) return null - - // File rows FK the new chat row, so the plan runs after the insert. - const { idMap, keyMap, blobTasks } = await planChatFileCopies({ - tx, - rows: sourceFiles, - newChatId: newId, - userId, - now, - }) - - const maps = { fileIds: idMap, fileKeys: keyMap } - if (idMap.size > 0 || keyMap.size > 0) { - await tx - .update(copilotChats) - .set({ resources: rewriteResourceFileRefs(parentResources, maps) }) - .where(eq(copilotChats.id, newId)) - } - - await appendCopilotChatMessages( - newId, - rewriteMessageFileRefs(messages, maps), - { chatModel: parent.model }, - tx - ) - return { row, blobTasks } - }) - - if (!result) { - return createInternalServerErrorResponse('Failed to create duplicated chat') - } - - const { copied, failed } = await executeChatFileBlobCopies(result.blobTasks, { - userId, - workspaceId: parent.workspaceId ?? undefined, - }) - if (failed > 0) { - logger.warn('Some chat file blobs failed to copy during duplicate', { - chatId, - newChatId: newId, - copied, - failed, - }) - } - - // Clone copilot-service conversation state (messages, active window, memory - // files) in whole-chat mode. Best-effort: if the copilot service doesn't - // have a row for the source chat yet (no messages sent), skip. - try { - const copilotHeaders: Record = { 'Content-Type': 'application/json' } - if (env.COPILOT_API_KEY) { - copilotHeaders['x-api-key'] = env.COPILOT_API_KEY - } - Object.assign(copilotHeaders, getMothershipSourceEnvHeaders()) - const mothershipBaseURL = await getMothershipBaseURL({ userId }) - const copilotRes = await fetchGo(`${mothershipBaseURL}/api/chats/fork`, { - method: 'POST', - headers: copilotHeaders, - body: JSON.stringify({ - sourceChatId: chatId, - newChatId: newId, - userId, - }), - spanName: 'sim → go /api/chats/fork', - operation: 'duplicate_chat', - }) - if (!copilotRes.ok) { - const text = await copilotRes.text().catch(() => '') - logger.warn('Copilot duplicate returned non-OK', { - status: copilotRes.status, - body: text, - }) - } - } catch (err) { - logger.warn('Failed to duplicate copilot-service conversation, skipping', { err }) - } - - if (result.row.workspaceId) { - chatPubSub?.publishStatusChanged({ - workspaceId: result.row.workspaceId, - chatId: newId, - type: 'created', - }) - } - - captureServerEvent( - userId, - 'task_duplicated', - { workspace_id: parent.workspaceId ?? '', source_chat_id: chatId }, - { groups: { workspace: parent.workspaceId ?? '' } } - ) - - return NextResponse.json({ success: true, id: newId }) - } catch (error) { - if (isWorkspaceAccessDeniedError(error)) { - return createForbiddenResponse('Workspace access denied') - } - logger.error('Error duplicating chat:', error) - return createInternalServerErrorResponse('Failed to duplicate chat') - } - } -) diff --git a/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.test.ts b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts similarity index 72% rename from apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.test.ts rename to apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts index b428f8ae4b6..d1a15936f71 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/duplicate/route.test.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts @@ -9,7 +9,7 @@ const { mockTransaction, mockSelectRows, mockCheckStorageQuota, - mockListDuplicableChatFiles, + mockListForkableChatFiles, mockPlanChatFileCopies, mockExecuteChatFileBlobCopies, mockLoadCopilotChatMessages, @@ -22,7 +22,7 @@ const { mockTransaction: vi.fn(), mockSelectRows: vi.fn(), mockCheckStorageQuota: vi.fn(), - mockListDuplicableChatFiles: vi.fn(), + mockListForkableChatFiles: vi.fn(), mockPlanChatFileCopies: vi.fn(), mockExecuteChatFileBlobCopies: vi.fn(), mockLoadCopilotChatMessages: vi.fn(), @@ -71,8 +71,8 @@ vi.mock('@/lib/billing/storage', () => ({ checkStorageQuota: mockCheckStorageQuota, })) -vi.mock('@/lib/copilot/chat/duplicate-chat-files', () => ({ - listDuplicableChatFiles: mockListDuplicableChatFiles, +vi.mock('@/lib/copilot/chat/fork-chat-files', () => ({ + listForkableChatFiles: mockListForkableChatFiles, planChatFileCopies: mockPlanChatFileCopies, executeChatFileBlobCopies: mockExecuteChatFileBlobCopies, })) @@ -109,7 +109,7 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({ isWorkspaceAccessDeniedError: () => false, })) -import { POST } from '@/app/api/mothership/chats/[chatId]/duplicate/route' +import { POST } from '@/app/api/mothership/chats/[chatId]/fork/route' const OLD_FILE_ID = 'wf_oldfile' const NEW_FILE_ID = 'wf_newfile' @@ -127,6 +127,27 @@ const parentRow = { config: null, } +const threeMessages = [ + { + id: 'msg-1', + role: 'user', + content: `See ![cat](/api/files/view/${OLD_FILE_ID})`, + timestamp: '2026-07-01T00:00:00.000Z', + }, + { + id: 'msg-2', + role: 'assistant', + content: 'Nice cat.', + timestamp: '2026-07-01T00:00:01.000Z', + }, + { + id: 'msg-3', + role: 'user', + content: 'A later message the fork must not keep.', + timestamp: '2026-07-01T00:00:02.000Z', + }, +] + function makeTx() { return { insert: () => ({ @@ -140,9 +161,11 @@ function makeTx() { } } -function createRequest(chatId: string) { - return new NextRequest(`http://localhost:3000/api/mothership/chats/${chatId}/duplicate`, { +function createRequest(chatId: string, body?: unknown) { + return new NextRequest(`http://localhost:3000/api/mothership/chats/${chatId}/fork`, { method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body ?? { upToMessageId: 'msg-2' }), }) } @@ -150,7 +173,7 @@ function makeContext(chatId: string) { return { params: Promise.resolve({ chatId }) } } -describe('POST /api/mothership/chats/[chatId]/duplicate', () => { +describe('POST /api/mothership/chats/[chatId]/fork', () => { beforeEach(() => { vi.clearAllMocks() copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({ @@ -158,9 +181,9 @@ describe('POST /api/mothership/chats/[chatId]/duplicate', () => { isAuthenticated: true, }) mockSelectRows.mockResolvedValue([parentRow]) - mockListDuplicableChatFiles.mockResolvedValue([]) + mockListForkableChatFiles.mockResolvedValue([]) mockCheckStorageQuota.mockResolvedValue({ allowed: true }) - mockLoadCopilotChatMessages.mockResolvedValue([]) + mockLoadCopilotChatMessages.mockResolvedValue(threeMessages) mockPlanChatFileCopies.mockResolvedValue({ idMap: new Map(), keyMap: new Map(), @@ -197,8 +220,37 @@ describe('POST /api/mothership/chats/[chatId]/duplicate', () => { expect(res.status).toBe(404) }) + it('400s when upToMessageId is missing', async () => { + const res = await POST(createRequest('chat-1', {}), makeContext('chat-1')) + expect(res.status).toBe(400) + expect(mockTransaction).not.toHaveBeenCalled() + }) + + it('400s when the message is not in the chat', async () => { + const res = await POST( + createRequest('chat-1', { upToMessageId: 'msg-unknown' }), + makeContext('chat-1') + ) + expect(res.status).toBe(400) + expect(mockTransaction).not.toHaveBeenCalled() + }) + + it('applies the timeline cut: kept message ids drive the file selection', async () => { + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + expect(res.status).toBe(200) + + // Files are selected by the kept slice (inclusive of msg-2, excluding msg-3). + const listCall = mockListForkableChatFiles.mock.calls[0] + expect(listCall[1]).toBe('chat-1') + expect(listCall[2]).toEqual(new Set(['msg-1', 'msg-2'])) + + // The appended transcript is the same inclusive slice. + const appended = mockAppendCopilotChatMessages.mock.calls[0] + expect(appended[1].map((m: { id: string }) => m.id)).toEqual(['msg-1', 'msg-2']) + }) + it('fails up front with the quota error when copied bytes would exceed the limit', async () => { - mockListDuplicableChatFiles.mockResolvedValue([{ size: 600 }, { size: 400 }]) + mockListForkableChatFiles.mockResolvedValue([{ size: 600 }, { size: 400 }]) mockCheckStorageQuota.mockResolvedValue({ allowed: false, error: 'Storage limit exceeded' }) const res = await POST(createRequest('chat-1'), makeContext('chat-1')) @@ -209,36 +261,28 @@ describe('POST /api/mothership/chats/[chatId]/duplicate', () => { expect(mockExecuteChatFileBlobCopies).not.toHaveBeenCalled() }) - it('skips the quota check entirely for a chat with no files', async () => { + it('skips the quota check entirely when no upload rows are in the cut', async () => { const res = await POST(createRequest('chat-1'), makeContext('chat-1')) expect(res.status).toBe(200) expect(mockCheckStorageQuota).not.toHaveBeenCalled() }) - it('duplicates the chat: copies files, rewrites references, clones agent state', async () => { + it('forks the chat: copies kept uploads, rewrites references, clones agent state', async () => { const blobTasks = [ { sourceKey: 'workspace/ws-1/old-cat.png', targetKey: 'workspace/ws-1/new-cat.png', - context: 'output', + context: 'mothership', fileName: 'cat.png', contentType: 'image/png', }, ] - mockListDuplicableChatFiles.mockResolvedValue([{ size: 100 }]) + mockListForkableChatFiles.mockResolvedValue([{ size: 100 }]) mockPlanChatFileCopies.mockResolvedValue({ idMap: new Map([[OLD_FILE_ID, NEW_FILE_ID]]), keyMap: new Map([['workspace/ws-1/old-cat.png', 'workspace/ws-1/new-cat.png']]), blobTasks, }) - mockLoadCopilotChatMessages.mockResolvedValue([ - { - id: 'msg-1', - role: 'assistant', - content: `![cat](/api/files/view/${OLD_FILE_ID})`, - timestamp: '2026-07-01T00:00:00.000Z', - }, - ]) const res = await POST(createRequest('chat-1'), makeContext('chat-1')) const body = await res.json() @@ -249,9 +293,10 @@ describe('POST /api/mothership/chats/[chatId]/duplicate', () => { expect(mockCheckStorageQuota).toHaveBeenCalledWith('user-1', 100) + // The real rewriter runs: the kept message's view-URL points at the copy. const appended = mockAppendCopilotChatMessages.mock.calls[0] expect(appended[0]).toBe(body.id) - expect(appended[1][0].content).toBe(`![cat](/api/files/view/${NEW_FILE_ID})`) + expect(appended[1][0].content).toBe(`See ![cat](/api/files/view/${NEW_FILE_ID})`) expect(mockExecuteChatFileBlobCopies).toHaveBeenCalledWith(blobTasks, { userId: 'user-1', @@ -261,8 +306,12 @@ describe('POST /api/mothership/chats/[chatId]/duplicate', () => { const goCall = mockFetchGo.mock.calls[0] expect(goCall[0]).toBe('http://mothership.test/api/chats/fork') const goBody = JSON.parse(goCall[1].body) - expect(goBody).toEqual({ sourceChatId: 'chat-1', newChatId: body.id, userId: 'user-1' }) - expect(goBody.upToMessageId).toBeUndefined() + expect(goBody).toEqual({ + sourceChatId: 'chat-1', + newChatId: body.id, + upToMessageId: 'msg-2', + userId: 'user-1', + }) expect(mockPublishStatusChanged).toHaveBeenCalledWith({ workspaceId: 'ws-1', @@ -271,7 +320,7 @@ describe('POST /api/mothership/chats/[chatId]/duplicate', () => { }) expect(mockCaptureServerEvent).toHaveBeenCalledWith( 'user-1', - 'task_duplicated', + 'task_forked', { workspace_id: 'ws-1', source_chat_id: 'chat-1' }, { groups: { workspace: 'ws-1' } } ) diff --git a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts index 44b63c7f163..d3be2fce208 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts @@ -6,8 +6,18 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { forkMothershipChatContract } from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' +import { checkStorageQuota } from '@/lib/billing/storage' +import { + executeChatFileBlobCopies, + listForkableChatFiles, + planChatFileCopies, +} from '@/lib/copilot/chat/fork-chat-files' import { loadCopilotChatMessages } from '@/lib/copilot/chat/lifecycle' import { appendCopilotChatMessages } from '@/lib/copilot/chat/messages-store' +import { + rewriteMessageFileRefs, + rewriteResourceFileRefs, +} from '@/lib/copilot/chat/rewrite-file-references' import { chatPubSub } from '@/lib/copilot/chat-status' import { fetchGo } from '@/lib/copilot/request/go/fetch' import { @@ -33,7 +43,12 @@ const logger = createLogger('ForkChatAPI') /** * POST /api/mothership/chats/[chatId]/fork * Creates a new chat branched from the given chat, keeping messages up to and - * including the specified message. Resources and copilot-side state are copied. + * including the specified message. Resources and copilot-side state are copied, + * along with the chat's uploads born at-or-before the fork point (fresh row id + * and storage key per copy, same message_id; bytes are physically copied and + * counted against the storage quota). Agent-generated outputs/ stay behind. + * Every in-transcript file reference is re-pointed at the copies so the fork + * survives deletion of the source chat. */ export const POST = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ chatId: string }> }) => { @@ -82,6 +97,18 @@ export const POST = withRouteHandler( } const forkedMessages = messages.slice(0, forkIdx + 1) + // The timeline cut for files: uploads born in any kept message come + // along; uploads born after the fork point stay behind. + const keptMessageIds = new Set(forkedMessages.map((m) => m.id)) + const sourceFiles = await listForkableChatFiles(db, chatId, keptMessageIds) + const totalFileBytes = sourceFiles.reduce((sum, row) => sum + row.size, 0) + if (totalFileBytes > 0) { + const quotaCheck = await checkStorageQuota(userId, totalFileBytes) + if (!quotaCheck.allowed) { + return createBadRequestResponse(quotaCheck.error || 'Storage limit exceeded') + } + } + // Resources are stored as a jsonb array on the chat row — copy them directly. const parentResources = Array.isArray(parent.resources) ? (parent.resources as MothershipResource[]) @@ -92,7 +119,7 @@ export const POST = withRouteHandler( const title = `Fork | ${baseTitle}` const now = new Date() - const newChat = await db.transaction(async (tx) => { + const result = await db.transaction(async (tx) => { const [row] = await tx .insert(copilotChats) .values({ @@ -114,13 +141,49 @@ export const POST = withRouteHandler( if (!row) return null - await appendCopilotChatMessages(newId, forkedMessages, { chatModel: parent.model }, tx) - return row + // File rows FK the new chat row, so the plan runs after the insert. + const { idMap, keyMap, blobTasks } = await planChatFileCopies({ + tx, + rows: sourceFiles, + newChatId: newId, + userId, + now, + }) + + const maps = { fileIds: idMap, fileKeys: keyMap } + if (idMap.size > 0 || keyMap.size > 0) { + await tx + .update(copilotChats) + .set({ resources: rewriteResourceFileRefs(parentResources, maps) }) + .where(eq(copilotChats.id, newId)) + } + + await appendCopilotChatMessages( + newId, + rewriteMessageFileRefs(forkedMessages, maps), + { chatModel: parent.model }, + tx + ) + return { row, blobTasks } }) - if (!newChat) { + if (!result) { return createInternalServerErrorResponse('Failed to create forked chat') } + const newChat = result.row + + const { copied, failed } = await executeChatFileBlobCopies(result.blobTasks, { + userId, + workspaceId: parent.workspaceId ?? undefined, + }) + if (failed > 0) { + logger.warn('Some chat file blobs failed to copy during fork', { + chatId, + newChatId: newId, + copied, + failed, + }) + } // Clone copilot-service conversation state (messages, active_messages, memory files). // Best-effort: if the copilot service doesn't have a row for the source chat yet, skip. diff --git a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx index 91ebc37ea42..d335ee75d97 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx @@ -162,7 +162,7 @@ export const MessageActions = memo(function MessageActions({ const hasContent = Boolean(content) const canSubmitFeedback = Boolean(chatId && userQuery) - const canFork = false + const canFork = Boolean(chatId && messageId) if (!hasContent && !canSubmitFeedback && !canFork) return null return ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 7143a5f27ae..2e94f37a861 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -92,7 +92,6 @@ import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' import { useDeleteMothershipChat, useDeleteMothershipChats, - useDuplicateMothershipChat, useMarkMothershipChatRead, useMarkMothershipChatUnread, useMothershipChats, @@ -598,7 +597,6 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { const deleteChatMutation = useDeleteMothershipChat(workspaceId) const deleteChatsMutation = useDeleteMothershipChats(workspaceId) - const duplicateChatMutation = useDuplicateMothershipChat(workspaceId) const markChatReadMutation = useMarkMothershipChatRead(workspaceId) const markChatUnreadMutation = useMarkMothershipChatUnread(workspaceId) const renameChatMutation = useRenameMothershipChat(workspaceId) @@ -965,20 +963,6 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { chatFlyoutRename.startRename({ id: chatId, name: chat.name }) }, [chatFlyoutRename, chats, chatsHover]) - const handleDuplicateChat = useCallback(() => { - const { chatIds: ids } = contextMenuSelectionRef.current - if (ids.length !== 1) return - duplicateChatMutation.mutate( - { chatId: ids[0] }, - { - onSuccess: (result) => { - useFolderStore.getState().clearChatSelection() - navigateToPage(`/workspace/${workspaceId}/chat/${result.id}`) - }, - } - ) - }, [navigateToPage, workspaceId]) - const handleToggleChatPin = useCallback(() => { const { chatIds: ids } = contextMenuSelectionRef.current if (ids.length !== 1) return @@ -1702,7 +1686,6 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { onMarkAsUnread={handleMarkChatAsUnread} onTogglePin={handleToggleChatPin} onRename={handleStartChatRename} - onDuplicate={handleDuplicateChat} onDelete={handleDeleteChat} showOpenInNewTab={!isMultiChatContextMenu} showMarkAsRead={!isMultiChatContextMenu && !!activeChatContextMenuItem?.isUnread} @@ -1714,9 +1697,8 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { showPin={!isMultiChatContextMenu && !!activeChatContextMenuItem} isPinned={!!activeChatContextMenuItem?.isPinned} showRename={!isMultiChatContextMenu} - showDuplicate={!isMultiChatContextMenu} + showDuplicate={false} disableRename={!canEdit} - disableDuplicate={!canEdit || duplicateChatMutation.isPending} disableDelete={!canEdit} /> diff --git a/apps/sim/hooks/queries/mothership-chats.ts b/apps/sim/hooks/queries/mothership-chats.ts index ec21cf94cfd..10e458c5aef 100644 --- a/apps/sim/hooks/queries/mothership-chats.ts +++ b/apps/sim/hooks/queries/mothership-chats.ts @@ -12,7 +12,6 @@ import { addMothershipChatResourceContract, createMothershipChatContract, deleteMothershipChatContract, - duplicateMothershipChatContract, forkMothershipChatContract, getMothershipChatContract, listMothershipChatsContract, @@ -719,46 +718,3 @@ export function useForkMothershipChat(workspaceId?: string) { }, }) } - -async function duplicateChat(params: { chatId: string }): Promise<{ id: string }> { - const data = await requestJson(duplicateMothershipChatContract, { - params: { chatId: params.chatId }, - }) - return { id: data.id } -} - -export function useDuplicateMothershipChat(workspaceId?: string) { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: duplicateChat, - onSuccess: async (data, variables) => { - if (!workspaceId) return - await queryClient.cancelQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) - const existing = queryClient.getQueryData( - mothershipChatKeys.list(workspaceId) - ) - if (existing) { - const sourceChat = existing.find((t) => t.id === variables.chatId) - const duplicateEntry: MothershipChatMetadata = { - id: data.id, - name: `${sourceChat?.name ?? 'New chat'} (Copy)`, - updatedAt: new Date(), - isActive: false, - isUnread: false, - isPinned: false, - } - const pinnedCount = existing.findIndex((chat) => !chat.isPinned) - const insertAt = pinnedCount === -1 ? existing.length : pinnedCount - queryClient.setQueryData(mothershipChatKeys.list(workspaceId), [ - ...existing.slice(0, insertAt), - duplicateEntry, - ...existing.slice(insertAt), - ]) - } - }, - onSettled: () => { - if (!workspaceId) return - queryClient.invalidateQueries({ queryKey: mothershipChatKeys.list(workspaceId) }) - }, - }) -} diff --git a/apps/sim/lib/api/contracts/mothership-chats.ts b/apps/sim/lib/api/contracts/mothership-chats.ts index fb567359030..7c8cf4f701f 100644 --- a/apps/sim/lib/api/contracts/mothership-chats.ts +++ b/apps/sim/lib/api/contracts/mothership-chats.ts @@ -294,19 +294,6 @@ export const listChatOutputsContract = defineRouteContract({ }, }) -export const duplicateMothershipChatContract = defineRouteContract({ - method: 'POST', - path: '/api/mothership/chats/[chatId]/duplicate', - params: mothershipChatParamsSchema, - response: { - mode: 'json', - schema: z.object({ - success: z.literal(true), - id: z.string(), - }), - }, -}) - export const createMothershipChatResponseSchema = z.object({ success: z.literal(true), id: z.string(), diff --git a/apps/sim/lib/copilot/chat/fork-chat-files.test.ts b/apps/sim/lib/copilot/chat/fork-chat-files.test.ts new file mode 100644 index 00000000000..c41d52d1b3e --- /dev/null +++ b/apps/sim/lib/copilot/chat/fork-chat-files.test.ts @@ -0,0 +1,173 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockGenerateKey, mockDownloadFile, mockUploadFile, mockIncrementStorageUsage } = vi.hoisted( + () => ({ + mockGenerateKey: vi.fn(), + mockDownloadFile: vi.fn(), + mockUploadFile: vi.fn(), + mockIncrementStorageUsage: vi.fn(), + }) +) + +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({ + generateWorkspaceFileKey: mockGenerateKey, +})) + +vi.mock('@/lib/uploads/core/storage-service', () => ({ + downloadFile: mockDownloadFile, + uploadFile: mockUploadFile, +})) + +vi.mock('@/lib/billing/storage', () => ({ + incrementStorageUsage: mockIncrementStorageUsage, +})) + +import { + executeChatFileBlobCopies, + type ForkableChatFileRow, + planChatFileCopies, +} from '@/lib/copilot/chat/fork-chat-files' + +const NOW = new Date('2026-07-02T00:00:00.000Z') + +function makeRow(overrides: Partial = {}): ForkableChatFileRow { + return { + id: 'wf_source', + key: 'workspace/ws-1/1-cat.png', + userId: 'user-1', + workspaceId: 'ws-1', + folderId: null, + context: 'mothership', + chatId: 'chat-1', + messageId: 'msg-1', + originalName: 'cat.png', + displayName: 'cat.png', + contentType: 'image/png', + size: 100, + deletedAt: null, + uploadedAt: new Date('2026-06-01T00:00:00.000Z'), + updatedAt: new Date('2026-06-01T00:00:00.000Z'), + ...overrides, + } as ForkableChatFileRow +} + +describe('planChatFileCopies', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGenerateKey.mockReturnValue('workspace/ws-1/2-cat.png') + }) + + it('copies a row under the fork with a fresh id + key and the SAME message_id', async () => { + const inserted: Array> = [] + const tx = { + insert: () => ({ + values: async (v: Record) => { + inserted.push(v) + }, + }), + } + + const { idMap, keyMap, blobTasks } = await planChatFileCopies({ + tx: tx as never, + rows: [makeRow()], + newChatId: 'chat-fork', + userId: 'user-1', + now: NOW, + }) + + expect(inserted).toHaveLength(1) + const copy = inserted[0] + expect(copy.id).not.toBe('wf_source') + expect(String(copy.id)).toMatch(/^wf_/) + expect(copy.key).toBe('workspace/ws-1/2-cat.png') + expect(copy.chatId).toBe('chat-fork') + expect(copy.messageId).toBe('msg-1') + expect(copy.displayName).toBe('cat.png') + expect(copy.deletedAt).toBeNull() + + expect(idMap.get('wf_source')).toBe(copy.id) + expect(keyMap.get('workspace/ws-1/1-cat.png')).toBe('workspace/ws-1/2-cat.png') + expect(blobTasks).toEqual([ + { + sourceKey: 'workspace/ws-1/1-cat.png', + targetKey: 'workspace/ws-1/2-cat.png', + context: 'mothership', + fileName: 'cat.png', + contentType: 'image/png', + }, + ]) + }) + + it('skips legacy rows with no workspaceId instead of failing the fork', async () => { + const inserted: Array> = [] + const tx = { + insert: () => ({ + values: async (v: Record) => { + inserted.push(v) + }, + }), + } + + const { idMap, blobTasks } = await planChatFileCopies({ + tx: tx as never, + rows: [makeRow({ workspaceId: null })], + newChatId: 'chat-fork', + userId: 'user-1', + now: NOW, + }) + + expect(inserted).toHaveLength(0) + expect(idMap.size).toBe(0) + expect(blobTasks).toHaveLength(0) + }) +}) + +describe('executeChatFileBlobCopies', () => { + const task = { + sourceKey: 'workspace/ws-1/1-cat.png', + targetKey: 'workspace/ws-1/2-cat.png', + context: 'mothership' as const, + fileName: 'cat.png', + contentType: 'image/png', + } + + beforeEach(() => { + vi.clearAllMocks() + mockDownloadFile.mockResolvedValue(Buffer.from('0123456789')) + mockUploadFile.mockResolvedValue(undefined) + mockIncrementStorageUsage.mockResolvedValue(undefined) + }) + + it('copies bytes to the new key and counts them against the storage quota', async () => { + const result = await executeChatFileBlobCopies([task], { + userId: 'user-1', + workspaceId: 'ws-1', + }) + + expect(result).toEqual({ copied: 1, failed: 0 }) + expect(mockUploadFile).toHaveBeenCalledWith( + expect.objectContaining({ + customKey: 'workspace/ws-1/2-cat.png', + preserveKey: true, + }) + ) + // No `metadata` in the upload call — passing it would insert a second row. + expect(mockUploadFile.mock.calls[0][0].metadata).toBeUndefined() + expect(mockIncrementStorageUsage).toHaveBeenCalledWith('user-1', 10, 'ws-1') + }) + + it('is best-effort: a failed download skips the file and counts nothing', async () => { + mockDownloadFile.mockRejectedValueOnce(new Error('blob missing')) + + const result = await executeChatFileBlobCopies([task, task], { + userId: 'user-1', + workspaceId: 'ws-1', + }) + + expect(result).toEqual({ copied: 1, failed: 1 }) + expect(mockIncrementStorageUsage).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/sim/lib/copilot/chat/duplicate-chat-files.ts b/apps/sim/lib/copilot/chat/fork-chat-files.ts similarity index 55% rename from apps/sim/lib/copilot/chat/duplicate-chat-files.ts rename to apps/sim/lib/copilot/chat/fork-chat-files.ts index 2ba2d8ad9a2..548ebc722f4 100644 --- a/apps/sim/lib/copilot/chat/duplicate-chat-files.ts +++ b/apps/sim/lib/copilot/chat/fork-chat-files.ts @@ -2,7 +2,7 @@ import { workspaceFiles } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' -import { and, eq, inArray, isNull } from 'drizzle-orm' +import { and, eq, inArray, isNull, or } from 'drizzle-orm' import { incrementStorageUsage } from '@/lib/billing/storage' import type { DbOrTx } from '@/lib/db/types' import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager' @@ -10,19 +10,20 @@ import { downloadFile, uploadFile } from '@/lib/uploads/core/storage-service' import type { StorageContext } from '@/lib/uploads/shared/types' import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' -const logger = createLogger('DuplicateChatFiles') +const logger = createLogger('ForkChatFiles') /** - * The chat-owned storage contexts a duplicate must copy: user uploads - * (`mothership`) and agent-generated outputs (`output`). Shared workspace - * `files/` (`context='workspace'`) is workspace-owned, not chat-owned — both - * chats reference it in place and it is never copied. + * The only chat-owned storage context a fork copies: user uploads + * (`mothership`). Agent-generated `outputs/` rows deliberately stay behind — a + * fork starts with an empty outputs/ namespace. Shared workspace `files/` + * (`context='workspace'`) is workspace-owned, not chat-owned — both chats + * reference it in place and it is never copied. */ -export const DUPLICABLE_CHAT_FILE_CONTEXTS: readonly StorageContext[] = ['mothership', 'output'] +export const FORKABLE_CHAT_FILE_CONTEXT: StorageContext = 'mothership' -export type DuplicableChatFileRow = typeof workspaceFiles.$inferSelect +export type ForkableChatFileRow = typeof workspaceFiles.$inferSelect -/** One blob byte-copy to run after the duplicate transaction commits. */ +/** One blob byte-copy to run after the fork transaction commits. */ export interface ChatBlobCopyTask { sourceKey: string targetKey: string @@ -41,39 +42,50 @@ export interface PlanChatFileCopiesResult { } /** - * The live chat-scoped file rows a duplicate copies: uploads + outputs owned by - * the chat, excluding soft-deleted rows. Also used pre-transaction to sum sizes - * for the storage-quota gate. + * The live upload rows a fork copies: the chat's `mothership`-context files + * whose `message_id` is at-or-before the fork point (i.e. in the kept message + * slice), excluding soft-deleted rows. Rows with a NULL `message_id` predate + * message tracking and are included in every fork of their chat — we can't + * know when they arrived, and copying them beats forking old chats with no + * files. Also used pre-transaction to sum sizes for the storage-quota gate. */ -export async function listDuplicableChatFiles( +export async function listForkableChatFiles( db: DbOrTx, - chatId: string -): Promise { + chatId: string, + keptMessageIds: ReadonlySet +): Promise { + const keptIds = [...keptMessageIds] + const messageCut = + keptIds.length > 0 + ? or(isNull(workspaceFiles.messageId), inArray(workspaceFiles.messageId, keptIds)) + : isNull(workspaceFiles.messageId) return db .select() .from(workspaceFiles) .where( and( eq(workspaceFiles.chatId, chatId), - inArray(workspaceFiles.context, [...DUPLICABLE_CHAT_FILE_CONTEXTS]), - isNull(workspaceFiles.deletedAt) + eq(workspaceFiles.context, FORKABLE_CHAT_FILE_CONTEXT), + isNull(workspaceFiles.deletedAt), + messageCut ) ) } /** - * Insert copy rows for the source chat's files under the new chat id (fresh - * `wf_` id + fresh storage key; display names carry over verbatim because their - * uniqueness is per-chat and the new chat is an empty namespace). Returns the - * old->new id/key maps that drive the reference rewrite, plus the blob - * byte-copies to run post-commit. Runs inside the duplicate transaction so a - * failed insert rolls the whole duplicate back; blob I/O is deferred to - * {@link executeChatFileBlobCopies}. Modeled on the workspace-fork copy - * (`lib/workspaces/fork/copy/copy-files.ts`), adapted for chat-scoped rows. + * Insert copy rows for the kept upload files under the new chat id (fresh + * `wf_` id + fresh storage key; `message_id` carries over verbatim so the copy + * matches the same message in the forked transcript; display names carry over + * verbatim because their uniqueness is per-chat and the new chat is an empty + * namespace). Returns the old->new id/key maps that drive the reference + * rewrite, plus the blob byte-copies to run post-commit. Runs inside the fork + * transaction so a failed insert rolls the whole fork back; blob I/O is + * deferred to {@link executeChatFileBlobCopies}. Modeled on the workspace-fork + * copy (`lib/workspaces/fork/copy/copy-files.ts`), adapted for chat-scoped rows. */ export async function planChatFileCopies(params: { tx: DbOrTx - rows: DuplicableChatFileRow[] + rows: ForkableChatFileRow[] newChatId: string userId: string now: Date @@ -85,7 +97,7 @@ export async function planChatFileCopies(params: { for (const row of rows) { if (!row.workspaceId) { - logger.warn('Skipping chat file with no workspaceId during duplicate', { fileId: row.id }) + logger.warn('Skipping chat file with no workspaceId during fork', { fileId: row.id }) continue } const copyId = `wf_${generateShortId()}` @@ -116,9 +128,9 @@ export async function planChatFileCopies(params: { /** * Copy each planned blob to its new key, best-effort: a failed copy logs a - * warning and is skipped (the duplicate keeps its transcript; that one file is - * missing) rather than failing the whole duplicate. Each successfully copied - * file increments the storage-usage counter by its actual byte length. + * warning and is skipped (the fork keeps its transcript; that one file is + * missing) rather than failing the whole fork. Each successfully copied file + * increments the storage-usage counter by its actual byte length. */ export async function executeChatFileBlobCopies( blobTasks: ChatBlobCopyTask[], @@ -146,22 +158,22 @@ export async function executeChatFileBlobCopies( }) copied += 1 try { - // Duplicated bytes COUNT against the storage quota, deliberately - // diverging from the workspace-fork copy path + // Forked bytes COUNT against the storage quota, deliberately diverging + // from the workspace-fork copy path // (lib/workspaces/fork/copy/copy-files.ts), which copies blobs without - // counting them. A chat duplicate stores a second physical copy of - // every upload/output, so the counter must reflect it. Do not "fix" - // this back to the fork precedent. + // counting them. A chat fork stores a second physical copy of every + // kept upload, so the counter must reflect it. Do not "fix" this back + // to the workspace-fork precedent. await incrementStorageUsage(params.userId, buffer.length, params.workspaceId) } catch (error) { - logger.error('Failed to update storage tracking for duplicated chat file', { + logger.error('Failed to update storage tracking for forked chat file', { targetKey: task.targetKey, error: getErrorMessage(error), }) } } catch (error) { failed += 1 - logger.warn('Failed to copy chat file blob during duplicate', { + logger.warn('Failed to copy chat file blob during fork', { sourceKey: task.sourceKey, targetKey: task.targetKey, error: getErrorMessage(error), diff --git a/apps/sim/lib/copilot/chat/payload.ts b/apps/sim/lib/copilot/chat/payload.ts index e718b33cce3..317f65b83bd 100644 --- a/apps/sim/lib/copilot/chat/payload.ts +++ b/apps/sim/lib/copilot/chat/payload.ts @@ -286,7 +286,8 @@ export async function buildCopilotRequestPayload( f.key, filename, mediaType, - f.size + f.size, + userMessageId ) // Encode the read path per the percent-encoded VFS convention (matches // files/ and the uploads glob output). The materialize_file `fileName` diff --git a/apps/sim/lib/copilot/tools/handlers/materialize-file.test.ts b/apps/sim/lib/copilot/tools/handlers/materialize-file.test.ts index 24fe894c1b7..b1c4b4878cb 100644 --- a/apps/sim/lib/copilot/tools/handlers/materialize-file.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/materialize-file.test.ts @@ -1,22 +1,32 @@ /** * @vitest-environment node */ +import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockFindUpload } = vi.hoisted(() => ({ +const { mockFindUpload, mockFindOutput, mockAllocateName } = vi.hoisted(() => ({ mockFindUpload: vi.fn(), + mockFindOutput: vi.fn(), + mockAllocateName: vi.fn(), })) +vi.mock('@sim/db', () => dbChainMock) + vi.mock('@/lib/copilot/tools/handlers/upload-file-reader', () => ({ findMothershipUploadRowByChatAndName: mockFindUpload, })) +vi.mock('@/lib/copilot/tools/handlers/output-file-reader', () => ({ + findChatOutputRowByChatAndName: mockFindOutput, +})) + vi.mock('@/lib/uploads', () => ({ getServePathPrefix: () => '/api/files/serve/', })) vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({ fetchWorkspaceFileBuffer: vi.fn(), + allocateUniqueWorkspaceFileName: mockAllocateName, })) vi.mock('@/lib/copilot/vfs/path-utils', () => ({ @@ -38,6 +48,39 @@ const context = { workflowId: 'wf-1', } as ExecutionContext +describe('executeMaterializeFile - save clears chat provenance', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('nulls both chatId and messageId when promoting an upload to the workspace', async () => { + mockFindUpload.mockResolvedValue({ + id: 'wf_1', + key: 'mothership/chat-1/cat.png', + workspaceId: 'ws-1', + folderId: null, + userId: 'user-1', + originalName: 'cat.png', + displayName: 'cat.png', + contentType: 'image/png', + size: 10, + deletedAt: null, + uploadedAt: new Date(), + updatedAt: new Date(), + }) + mockAllocateName.mockResolvedValue('cat.png') + dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'wf_1', originalName: 'cat.png' }]) + + const result = await executeMaterializeFile({ fileNames: ['cat.png'] }, context) + + expect(result.success).toBe(true) + expect(dbChainMockFns.set).toHaveBeenCalledWith( + expect.objectContaining({ context: 'workspace', chatId: null, messageId: null }) + ) + }) +}) + describe('executeMaterializeFile - unsupported operation', () => { beforeEach(() => vi.clearAllMocks()) diff --git a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts index 32eeef8ce63..5c9c9139e1d 100644 --- a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts +++ b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts @@ -67,7 +67,10 @@ async function executeSave(fileName: string, chatId: string): Promise { ) }) + it('stamps message_id on the UPDATE arm when the birth message is known', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'wf_existing' }]) + + await trackChatUpload( + WORKSPACE_ID, + USER_ID, + CHAT_ID, + S3_KEY, + 'image.png', + 'image/png', + 1024, + 'msg_abc' + ) + + expect(dbChainMockFns.set).toHaveBeenCalledWith( + expect.objectContaining({ chatId: CHAT_ID, messageId: 'msg_abc' }) + ) + }) + + it('stamps message_id on the fallback INSERT arm and nulls it when omitted', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([]) + + await trackChatUpload( + WORKSPACE_ID, + USER_ID, + CHAT_ID, + S3_KEY, + 'image.png', + 'image/png', + 1024, + 'msg_abc' + ) + + expect(dbChainMockFns.values).toHaveBeenCalledWith( + expect.objectContaining({ chatId: CHAT_ID, messageId: 'msg_abc' }) + ) + + // Legacy callers without a message id write an explicit NULL ("birth unknown"). + dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'wf_existing' }]) + await trackChatUpload(WORKSPACE_ID, USER_ID, CHAT_ID, S3_KEY, 'image.png', 'image/png', 1024) + expect(dbChainMockFns.set).toHaveBeenLastCalledWith( + expect.objectContaining({ messageId: null }) + ) + }) + it('retries with a suffixed displayName on collision against the chat displayName index', async () => { // 23505 from the partial unique index on (chat_id, display_name) — the case we retry. const displayNameCollision = Object.assign(new Error('duplicate key'), { diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index b8eafc247c3..9c0b444e53d 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -487,14 +487,20 @@ export async function trackChatUpload( s3Key: string, fileName: string, contentType: string, - size: number + size: number, + messageId?: string ): Promise<{ displayName: string }> { for (let n = 1; n <= MAX_CHAT_DISPLAY_NAME_RETRIES; n++) { const candidate = suffixedName(fileName, n) try { const updated = await db .update(workspaceFiles) - .set({ chatId, context: 'mothership', displayName: candidate }) + .set({ + chatId, + messageId: messageId ?? null, + context: 'mothership', + displayName: candidate, + }) .where( and( eq(workspaceFiles.key, s3Key), @@ -520,6 +526,7 @@ export async function trackChatUpload( workspaceId, context: 'mothership', chatId, + messageId: messageId ?? null, originalName: fileName, displayName: candidate, contentType, diff --git a/packages/db/migrations/0254_classy_madame_hydra.sql b/packages/db/migrations/0254_classy_madame_hydra.sql new file mode 100644 index 00000000000..180d5266afa --- /dev/null +++ b/packages/db/migrations/0254_classy_madame_hydra.sql @@ -0,0 +1 @@ +ALTER TABLE "workspace_files" ADD COLUMN "message_id" text; \ No newline at end of file diff --git a/packages/db/migrations/meta/0254_snapshot.json b/packages/db/migrations/meta/0254_snapshot.json new file mode 100644 index 00000000000..f2ae314cdf9 --- /dev/null +++ b/packages/db/migrations/meta/0254_snapshot.json @@ -0,0 +1,17873 @@ +{ + "id": "c20f9c87-02f4-492b-a902-81cfe07a93c2", + "prevId": "5fb249c9-5269-456d-83bd-7a2d1bac7a22", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": [ + "certificate_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": [ + "env_owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": [ + "credential_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": [ + "accepted_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": [ + "credential_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": [ + "drain_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": [ + "knowledge_base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": [ + "connector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": [ + "knowledge_base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": [ + "parent_key", + "child_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": [ + "key", + "execution_id", + "source" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": [ + "invitation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": [ + "knowledge_base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": [ + "knowledge_base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": [ + "connector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": [ + "mcp_server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": [ + "added_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": [ + "set_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": [ + "permission_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": [ + "assigned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": [ + "permission_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": [ + "paused_execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": [ + "active_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": [ + "workflow_id", + "block_id", + "scope_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": [ + "row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": [ + "row_id", + "group_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": [ + "triggered_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": [ + "normalized_email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": [ + "deployment_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": [ + "credential_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": [ + "source_block_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": [ + "target_block_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": [ + "state_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": [ + "deployment_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": [ + "deployment_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": [ + "source_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": [ + "source_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "billed_account_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": [ + "forked_from_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": [ + "child_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": [ + "child_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": [ + "child_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": [ + "child_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": [ + "active", + "revoked", + "expired" + ] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": [ + "deployment_side_effects", + "fork_content_copy", + "fork_sync", + "fork_rollback" + ] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": [ + "pending", + "processing", + "completed", + "completed_with_warnings", + "failed" + ] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": [ + "payment_failed", + "dispute" + ] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": [ + "user", + "organization" + ] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": [ + "mothership", + "copilot" + ] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": [ + "pending", + "running", + "completed", + "failed", + "cancelled", + "delivered" + ] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": [ + "active", + "paused_waiting_for_tool", + "resuming", + "complete", + "error", + "cancelled" + ] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": [ + "admin", + "member" + ] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": [ + "active", + "pending", + "revoked" + ] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": [ + "pending", + "accepted", + "expired", + "cancelled" + ] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": [ + "active", + "pending", + "revoked" + ] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": [ + "oauth", + "env_workspace", + "env_personal", + "service_account" + ] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": [ + "hourly", + "daily" + ] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": [ + "s3", + "gcs", + "azure_blob", + "datadog", + "bigquery", + "snowflake", + "webhook" + ] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": [ + "running", + "success", + "failed" + ] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": [ + "cron", + "manual" + ] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": [ + "workflow_logs", + "job_logs", + "audit_logs", + "copilot_chats", + "copilot_runs" + ] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": [ + "execution_log", + "paused_snapshot" + ] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": [ + "organization", + "workspace" + ] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": [ + "internal", + "external" + ] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": [ + "pending", + "accepted", + "rejected", + "cancelled", + "expired" + ] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": [ + "admin", + "write", + "read" + ] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": [ + "model", + "fixed", + "tool" + ] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": [ + "push", + "pull" + ] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "custom_tool", + "skill" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": [ + "personal", + "organization", + "grandfathered_shared" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 61a3560d9e2..55c100fe938 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1772,6 +1772,13 @@ "when": 1782860073503, "tag": "0253_canonical_trigger_provider_config", "breakpoints": true + }, + { + "idx": 254, + "version": "7", + "when": 1783042186618, + "tag": "0254_classy_madame_hydra", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 383c1a3120a..fe31cd2ecb5 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1642,6 +1642,16 @@ export const workspaceFiles = pgTable( }), context: text('context').notNull(), // 'workspace', 'mothership', 'copilot', 'chat', 'knowledge-base', 'profile-pictures', 'general', 'execution' chatId: uuid('chat_id').references(() => copilotChats.id, { onDelete: 'cascade' }), + /** + * Logical id of the copilot message this file was born in (the user message the + * upload was attached to). Plain text with no FK: message ids are only unique per + * chat — the same id legitimately exists in the source chat and every fork of it, + * which is what lets a fork's "copy files at-or-before this message" cut match rows + * in both. NULL means "birth unknown / not tracked": rows predating this column and + * contexts that don't stamp it (e.g. 'output'). Nulled together with chatId when a + * file is materialized to the workspace. + */ + messageId: text('message_id'), originalName: text('original_name').notNull(), /** * Collision-disambiguated name exposed to the copilot VFS as `uploads/`. diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 9f5d387d0a1..80f935786b0 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 884, - zodRoutes: 884, + totalRoutes: 883, + zodRoutes: 883, nonZodRoutes: 0, } as const From 834dc2e7b73e980d7a78e5b4c5124ea6dcaf08ee Mon Sep 17 00:00:00 2001 From: Justin Blumencranz Date: Thu, 2 Jul 2026 19:56:19 -0700 Subject: [PATCH 05/10] fix(chat): stop phantom-stream reconnect wedge after a failed send MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a #wsres-file outputs/ link minted a resource with an empty id (the outputs lookup ran with the route chatId, which stays undefined on the home surface), which was persisted and attached to the next send. The chat POST then 400'd on attachment validation before creating a run, and the send's catch "recovered" by reconnecting to its own never- registered stream id — 10 backoff retries against stream_not_found, ~3 minutes of stuck "running" UI with the real error swallowed. - resolve outputs/ file links with the stream-resolved chat id, and drop file resources that still have no id after resolution - reject empty-id resources in addResource, hydration merge, and the send's resourceAttachments - only retry-reconnect when the stream actually started; a failed POST now rolls back the optimistic send and surfaces the error - treat resume 404 (stream_not_found) as terminal instead of retrying - require min(1) resource ids in the add/reorder contracts (remove stays permissive so legacy empty-id rows can be deleted) Co-Authored-By: Claude Fable 5 --- .../app/workspace/[workspaceId]/home/home.tsx | 17 +++++- .../[workspaceId]/home/hooks/use-chat.ts | 58 ++++++++++++++++++- apps/sim/lib/api/contracts/copilot.ts | 6 +- 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index bf3e5303732..07c62b20c26 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -124,9 +124,6 @@ export function Home({ chatId, userName, userId }: HomeProps) { ) const firstName = userName?.split(' ')[0] ?? '' const { data: workspaceFiles = [] } = useWorkspaceFiles(workspaceId) - // Chat-scoped agent outputs aren't in the workspace list; used to resolve an - // `outputs/...` file reference (e.g. a #wsres-file link) to its real file id. - const { data: chatOutputs = [] } = useChatOutputs(chatId) const { data: workflows = [] } = useWorkflows(workspaceId) const { data: folders = [] } = useFolders(workspaceId) const posthog = usePostHog() @@ -256,6 +253,13 @@ export function Home({ chatId, userName, userId }: HomeProps) { }) ) + // Chat-scoped agent outputs aren't in the workspace list; used to resolve an + // `outputs/...` file reference (e.g. a #wsres-file link) to its real file id. + // Falls back to the stream-resolved chat id: on the home surface the route + // never changes (the URL is rewritten via history.replaceState), so `chatId` + // stays undefined even after the chat exists server-side. + const { data: chatOutputs = [] } = useChatOutputs(chatId ?? resolvedChatId) + useEffect(() => { wasSendingRef.current = false if (resolvedChatId) { @@ -451,6 +455,13 @@ export function Home({ chatId, userName, userId }: HomeProps) { function handleWorkspaceResourceSelect(resource: MothershipResource) { const resolvedResource = resolveFileResource(resource) + // A #wsres-file link carries only a path; if it didn't resolve to a real + // file id, adding it would persist a broken `{id: ''}` resource that later + // fails chat-send validation. + if (resolvedResource.type === 'file' && !resolvedResource.id) { + logger.warn('Ignoring file resource with unresolved id', { path: resolvedResource.path }) + return + } const wasAdded = addResource(resolvedResource) if (!wasAdded) { setActiveResourceId(resolvedResource.id) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index e81ec0603f9..b9bc03cca38 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -192,6 +192,17 @@ interface ActiveQueuedSendHandoffRecovery { ownerId: string } +/** + * The resume endpoint 404'd: no run row exists for this stream id. Terminal — + * retrying can never succeed, since runs are created before streaming begins. + */ +class StreamNotFoundError extends Error { + constructor(streamId: string) { + super(`Stream not found: ${streamId}`) + this.name = 'StreamNotFoundError' + } +} + function createTimeoutSignal(ms: number): AbortSignal | undefined { if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') { return AbortSignal.timeout(ms) @@ -1469,6 +1480,11 @@ export function useChat( return source.map((m) => restoreRevealedSimKeysForMessage(m, revealedSimKeysRef.current)) }, [chatHistory, pendingMessages]) const addResource = useCallback((resource: MothershipResource): boolean => { + // An id-less resource can't be opened, persisted, or attached to a send — + // and once persisted it fails chat POST validation on every later message. + if (!resource.id) { + return false + } if (resourcesRef.current.some((r) => r.type === resource.type && r.id === resource.id)) { return false } @@ -1776,7 +1792,11 @@ export function useChat( flushPendingResources(chatHistory.id) - const persistedResources = chatHistory.resources.filter((r) => r.id !== 'streaming-file') + // Also drop legacy empty-id rows (persisted before ids were validated) so + // they can't re-enter local state and poison sends/reorders. + const persistedResources = chatHistory.resources.filter( + (r) => r.id && r.id !== 'streaming-file' + ) const serverKeys = new Set(persistedResources.map((r) => `${r.type}:${r.id}`)) const localOnly = resourcesRef.current.filter( (r) => r.id !== 'streaming-file' && !serverKeys.has(`${r.type}:${r.id}`) @@ -2109,6 +2129,9 @@ export function useChat( : {}), } ) + if (response.status === 404) { + throw new StreamNotFoundError(streamId) + } if (!response.ok) { throw new Error(`Stream resume batch failed: ${response.status}`) } @@ -2147,6 +2170,9 @@ export function useChat( if (chatId) return chatId } catch (error) { lastError = error + if (error instanceof StreamNotFoundError) { + break + } if (error instanceof Error && error.name === 'AbortError' && Date.now() >= deadline) { break } @@ -2549,6 +2575,16 @@ export function useChat( } return false } + if (err instanceof StreamNotFoundError) { + logger.error('Reconnect halted: stream does not exist on the server', { + streamId, + attempt: attempt + 1, + }) + if (streamGenRef.current === gen) { + setIsReconnecting(false) + } + return false + } logger.warn('Reconnect attempt failed', { streamId, attempt: attempt + 1, @@ -3133,6 +3169,9 @@ export function useChat( let gen: number | undefined let streamTargetChatId: string | undefined + // Flips once the chat POST returns OK — before that, streamIdRef holds + // THIS send's own (never-registered) id, so "reconnect" is meaningless. + let streamStarted = false try { if (pendingStop) { try { @@ -3204,7 +3243,11 @@ export function useChat( abortControllerRef.current = abortController const currentActiveId = activeResourceIdRef.current - const currentResources = resourcesRef.current + // Placeholder/broken tabs (streaming-file, empty id) fail the chat + // POST's attachment validation server-side — never send them. + const currentResources = resourcesRef.current.filter( + (r) => r.id && r.id !== 'streaming-file' + ) const resourceAttachments = currentResources.length > 0 ? currentResources.map((r) => ({ @@ -3288,6 +3331,8 @@ export function useChat( throw new Error(errorData.error || `Request failed: ${response.status}`) } + streamStarted = true + if (queuedSendHandoff) { clearQueuedSendHandoffState(queuedSendHandoff.id) } @@ -3343,7 +3388,7 @@ export function useChat( } const activeStreamId = streamIdRef.current - if (activeStreamId && gen !== undefined && streamGenRef.current === gen) { + if (streamStarted && activeStreamId && gen !== undefined && streamGenRef.current === gen) { const succeeded = await retryReconnect({ streamId: activeStreamId, assistantId, @@ -3353,6 +3398,13 @@ export function useChat( if (succeeded) return consumedByTranscript } + if (!streamStarted && (gen === undefined || streamGenRef.current === gen)) { + // The POST itself failed — no server-side stream exists, so undo the + // optimistic message and the cache's phantom activeStreamId instead + // of leaving state that later hydrations would try to "reconnect" to. + rollbackOptimisticSend() + } + setError(getErrorMessage(err, 'Failed to send message')) if (gen !== undefined && streamGenRef.current === gen) { finalize({ diff --git a/apps/sim/lib/api/contracts/copilot.ts b/apps/sim/lib/api/contracts/copilot.ts index f522728f647..6e9f8ec52cf 100644 --- a/apps/sim/lib/api/contracts/copilot.ts +++ b/apps/sim/lib/api/contracts/copilot.ts @@ -100,7 +100,7 @@ export const addCopilotChatResourceBodySchema = z.object({ chatId: z.string(), resource: z.object({ type: copilotResourceTypeSchema, - id: z.string(), + id: z.string().min(1, 'resource id is required'), title: z.string(), }), }) @@ -113,12 +113,14 @@ export const removeCopilotChatResourceBodySchema = z.object({ }) export type RemoveCopilotChatResourceBody = z.input +// `removeCopilotChatResourceBodySchema` above intentionally keeps a permissive +// `resourceId` so legacy empty-id rows can still be deleted. export const reorderCopilotChatResourcesBodySchema = z.object({ chatId: z.string(), resources: z.array( z.object({ type: copilotResourceTypeSchema, - id: z.string(), + id: z.string().min(1, 'resource id is required'), title: z.string(), }) ), From d10be732b581e988ba34f3f88ad082e05cfdd342 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:35:05 -0700 Subject: [PATCH 06/10] feat(chat): duplicate a chat via whole-chat fork mode --- .../chats/[chatId]/fork/route.test.ts | 198 +++++++++++++++++- .../mothership/chats/[chatId]/fork/route.ts | 93 +++++--- .../message-actions/message-actions.tsx | 6 +- .../w/components/sidebar/sidebar.tsx | 21 +- apps/sim/hooks/queries/mothership-chats.ts | 18 +- .../sim/lib/api/contracts/mothership-chats.ts | 8 +- .../copilot/chat/effective-transcript.test.ts | 9 + .../lib/copilot/chat/effective-transcript.ts | 10 + .../lib/copilot/chat/fork-chat-files.test.ts | 33 +++ apps/sim/lib/copilot/chat/fork-chat-files.ts | 36 +++- .../chat/rewrite-file-references.test.ts | 34 +++ .../copilot/chat/rewrite-file-references.ts | 24 ++- .../tools/handlers/output-file-reader.ts | 13 ++ .../tools/handlers/upload-file-reader.ts | 13 ++ .../server/files/resolve-input-file.test.ts | 90 ++++++++ .../tools/server/files/resolve-input-file.ts | 34 +++ .../tools/server/image/generate-image.ts | 60 +++--- .../lib/copilot/tools/server/media/ffmpeg.ts | 12 +- .../tools/server/media/generate-audio.ts | 12 +- .../tools/server/media/generate-video.ts | 12 +- apps/sim/lib/posthog/events.ts | 2 + 21 files changed, 652 insertions(+), 86 deletions(-) create mode 100644 apps/sim/lib/copilot/tools/server/files/resolve-input-file.test.ts create mode 100644 apps/sim/lib/copilot/tools/server/files/resolve-input-file.ts diff --git a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts index d1a15936f71..40d3325899f 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts @@ -10,6 +10,7 @@ const { mockSelectRows, mockCheckStorageQuota, mockListForkableChatFiles, + mockListDuplicableChatFiles, mockPlanChatFileCopies, mockExecuteChatFileBlobCopies, mockLoadCopilotChatMessages, @@ -23,6 +24,7 @@ const { mockSelectRows: vi.fn(), mockCheckStorageQuota: vi.fn(), mockListForkableChatFiles: vi.fn(), + mockListDuplicableChatFiles: vi.fn(), mockPlanChatFileCopies: vi.fn(), mockExecuteChatFileBlobCopies: vi.fn(), mockLoadCopilotChatMessages: vi.fn(), @@ -73,6 +75,7 @@ vi.mock('@/lib/billing/storage', () => ({ vi.mock('@/lib/copilot/chat/fork-chat-files', () => ({ listForkableChatFiles: mockListForkableChatFiles, + listDuplicableChatFiles: mockListDuplicableChatFiles, planChatFileCopies: mockPlanChatFileCopies, executeChatFileBlobCopies: mockExecuteChatFileBlobCopies, })) @@ -148,15 +151,26 @@ const threeMessages = [ }, ] +/** Chat rows inserted through the mock transaction, captured for title assertions. */ +let insertedChatRows: Array> = [] +/** tx.update(...).set(...) payloads, captured for resource-rewrite assertions. */ +let updatedChatRows: Array> = [] + function makeTx() { return { insert: () => ({ - values: () => ({ - returning: async () => [{ id: 'row-id', workspaceId: 'ws-1' }], - }), + values: (v: Record) => { + insertedChatRows.push(v) + return { + returning: async () => [{ id: 'row-id', workspaceId: 'ws-1' }], + } + }, }), update: () => ({ - set: vi.fn().mockReturnValue({ where: async () => undefined }), + set: (v: Record) => { + updatedChatRows.push(v) + return { where: async () => undefined } + }, }), } } @@ -176,12 +190,15 @@ function makeContext(chatId: string) { describe('POST /api/mothership/chats/[chatId]/fork', () => { beforeEach(() => { vi.clearAllMocks() + insertedChatRows = [] + updatedChatRows = [] copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({ userId: 'user-1', isAuthenticated: true, }) mockSelectRows.mockResolvedValue([parentRow]) mockListForkableChatFiles.mockResolvedValue([]) + mockListDuplicableChatFiles.mockResolvedValue([]) mockCheckStorageQuota.mockResolvedValue({ allowed: true }) mockLoadCopilotChatMessages.mockResolvedValue(threeMessages) mockPlanChatFileCopies.mockResolvedValue({ @@ -220,8 +237,8 @@ describe('POST /api/mothership/chats/[chatId]/fork', () => { expect(res.status).toBe(404) }) - it('400s when upToMessageId is missing', async () => { - const res = await POST(createRequest('chat-1', {}), makeContext('chat-1')) + it('400s when upToMessageId is an empty string', async () => { + const res = await POST(createRequest('chat-1', { upToMessageId: '' }), makeContext('chat-1')) expect(res.status).toBe(400) expect(mockTransaction).not.toHaveBeenCalled() }) @@ -321,9 +338,12 @@ describe('POST /api/mothership/chats/[chatId]/fork', () => { expect(mockCaptureServerEvent).toHaveBeenCalledWith( 'user-1', 'task_forked', - { workspace_id: 'ws-1', source_chat_id: 'chat-1' }, + { workspace_id: 'ws-1', source_chat_id: 'chat-1', whole_chat: false }, { groups: { workspace: 'ws-1' } } ) + + // Branch forks are titled "Fork | ". + expect(insertedChatRows[0].title).toBe('Fork | Generate Logs') }) it('still succeeds when the copilot-service clone fails (best-effort)', async () => { @@ -331,4 +351,168 @@ describe('POST /api/mothership/chats/[chatId]/fork', () => { const res = await POST(createRequest('chat-1'), makeContext('chat-1')) expect(res.status).toBe(200) }) + + it('drops ghost resources on a branch fork: chat-owned files that were not copied', async () => { + // The source chat generated two outputs (apple pre-cut, banana post-cut) + // and has one upload + one shared workspace-file resource. A branch fork + // copies only the kept upload; BOTH outputs stay behind, so both output + // resources must be dropped — not left pointing at the source chat. + mockSelectRows.mockResolvedValue([ + { + ...parentRow, + resources: [ + { type: 'file', id: OLD_FILE_ID, title: 'cat.png' }, + { type: 'file', id: 'wf_apple_output', title: 'apple.png' }, + { type: 'file', id: 'wf_banana_output', title: 'banana.png' }, + { type: 'file', id: 'wf_shared', title: 'shared.pdf' }, + { type: 'workflow', id: 'wflow-1', title: 'My flow' }, + ], + }, + ]) + // Every chat-owned file of the source chat (uploads + outputs, no cut). + mockListDuplicableChatFiles.mockResolvedValue([ + { id: OLD_FILE_ID }, + { id: 'wf_apple_output' }, + { id: 'wf_banana_output' }, + ]) + // The branch cut keeps (and the plan copies) only the upload. + mockListForkableChatFiles.mockResolvedValue([{ id: OLD_FILE_ID, size: 100 }]) + mockPlanChatFileCopies.mockResolvedValue({ + idMap: new Map([[OLD_FILE_ID, NEW_FILE_ID]]), + keyMap: new Map(), + blobTasks: [], + }) + + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + + expect(res.status).toBe(200) + expect(updatedChatRows).toHaveLength(1) + expect(updatedChatRows[0].resources).toEqual([ + { type: 'file', id: NEW_FILE_ID, title: 'cat.png' }, + { type: 'file', id: 'wf_shared', title: 'shared.pdf' }, + { type: 'workflow', id: 'wflow-1', title: 'My flow' }, + ]) + }) + + it('drops ghosts even when the fork copies no files at all', async () => { + // Fork cut before any upload, but the source chat has an output: the + // old guard skipped the resources update entirely when idMap was empty, + // leaving the ghost in place. + mockSelectRows.mockResolvedValue([ + { + ...parentRow, + resources: [{ type: 'file', id: 'wf_banana_output', title: 'banana.png' }], + }, + ]) + mockListDuplicableChatFiles.mockResolvedValue([{ id: 'wf_banana_output' }]) + mockListForkableChatFiles.mockResolvedValue([]) + + const res = await POST(createRequest('chat-1'), makeContext('chat-1')) + + expect(res.status).toBe(200) + expect(updatedChatRows).toHaveLength(1) + expect(updatedChatRows[0].resources).toEqual([]) + }) + + describe('whole-chat duplicate (no upToMessageId)', () => { + it('keeps every message and copies files with no timeline cut', async () => { + const res = await POST(createRequest('chat-1', {}), makeContext('chat-1')) + const body = await res.json() + + expect(res.status).toBe(200) + expect(body.success).toBe(true) + + // The whole-chat lister runs (uploads AND outputs, no message cut) — the + // branch lister never does. + expect(mockListDuplicableChatFiles).toHaveBeenCalledTimes(1) + expect(mockListDuplicableChatFiles.mock.calls[0][1]).toBe('chat-1') + expect(mockListForkableChatFiles).not.toHaveBeenCalled() + + // Every message is appended — including msg-3, which a branch would cut. + const appended = mockAppendCopilotChatMessages.mock.calls[0] + expect(appended[1].map((m: { id: string }) => m.id)).toEqual(['msg-1', 'msg-2', 'msg-3']) + }) + + it('titles the copy " (Copy)"', async () => { + const res = await POST(createRequest('chat-1', {}), makeContext('chat-1')) + expect(res.status).toBe(200) + expect(insertedChatRows[0].title).toBe('Generate Logs (Copy)') + }) + + it('asks the copilot service for whole-chat mode (no upToMessageId in the body)', async () => { + const res = await POST(createRequest('chat-1', {}), makeContext('chat-1')) + const body = await res.json() + expect(res.status).toBe(200) + + const goBody = JSON.parse(mockFetchGo.mock.calls[0][1].body) + expect(goBody).toEqual({ + sourceChatId: 'chat-1', + newChatId: body.id, + userId: 'user-1', + }) + expect('upToMessageId' in goBody).toBe(false) + + expect(mockCaptureServerEvent).toHaveBeenCalledWith( + 'user-1', + 'task_forked', + { workspace_id: 'ws-1', source_chat_id: 'chat-1', whole_chat: true }, + { groups: { workspace: 'ws-1' } } + ) + }) + + it('gates the quota on the full upload + output byte total', async () => { + mockListDuplicableChatFiles.mockResolvedValue([ + { size: 700, context: 'mothership' }, + { size: 500, context: 'output' }, + ]) + mockCheckStorageQuota.mockResolvedValue({ allowed: false, error: 'Storage limit exceeded' }) + + const res = await POST(createRequest('chat-1', {}), makeContext('chat-1')) + + expect(res.status).toBe(400) + expect(mockCheckStorageQuota).toHaveBeenCalledWith('user-1', 1200) + expect(mockTransaction).not.toHaveBeenCalled() + }) + + it('duplicates an empty chat cleanly', async () => { + mockLoadCopilotChatMessages.mockResolvedValue([]) + + const res = await POST(createRequest('chat-1', {}), makeContext('chat-1')) + + expect(res.status).toBe(200) + expect(mockAppendCopilotChatMessages.mock.calls[0][1]).toEqual([]) + }) + + it('keeps every resource: all chat-owned files are copied, so none are ghosts', async () => { + mockSelectRows.mockResolvedValue([ + { + ...parentRow, + resources: [ + { type: 'file', id: OLD_FILE_ID, title: 'cat.png' }, + { type: 'file', id: 'wf_banana_output', title: 'banana.png' }, + ], + }, + ]) + mockListDuplicableChatFiles.mockResolvedValue([ + { id: OLD_FILE_ID, size: 100 }, + { id: 'wf_banana_output', size: 200 }, + ]) + mockPlanChatFileCopies.mockResolvedValue({ + idMap: new Map([ + [OLD_FILE_ID, NEW_FILE_ID], + ['wf_banana_output', 'wf_banana_copy'], + ]), + keyMap: new Map(), + blobTasks: [], + }) + + const res = await POST(createRequest('chat-1', {}), makeContext('chat-1')) + + expect(res.status).toBe(200) + expect(updatedChatRows[0].resources).toEqual([ + { type: 'file', id: NEW_FILE_ID, title: 'cat.png' }, + { type: 'file', id: 'wf_banana_copy', title: 'banana.png' }, + ]) + }) + }) }) diff --git a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts index d3be2fce208..148293121ea 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts @@ -9,6 +9,7 @@ import { parseRequest } from '@/lib/api/server' import { checkStorageQuota } from '@/lib/billing/storage' import { executeChatFileBlobCopies, + listDuplicableChatFiles, listForkableChatFiles, planChatFileCopies, } from '@/lib/copilot/chat/fork-chat-files' @@ -42,13 +43,24 @@ const logger = createLogger('ForkChatAPI') /** * POST /api/mothership/chats/[chatId]/fork - * Creates a new chat branched from the given chat, keeping messages up to and - * including the specified message. Resources and copilot-side state are copied, - * along with the chat's uploads born at-or-before the fork point (fresh row id - * and storage key per copy, same message_id; bytes are physically copied and - * counted against the storage quota). Agent-generated outputs/ stay behind. - * Every in-transcript file reference is re-pointed at the copies so the fork - * survives deletion of the source chat. + * Creates a new chat copied from the given chat, in one of two modes. + * + * Branch (upToMessageId set): keeps messages up to and including the specified + * message, along with the chat's uploads born at-or-before the fork point. + * Agent-generated outputs/ stay behind, and the copy is titled "Fork | ". + * + * Whole-chat duplicate (upToMessageId omitted): keeps every message, copies + * uploads AND outputs, titles the copy " (Copy)", and asks the copilot + * service for its whole-chat clone mode (compacted working memory preserved + * verbatim — nothing is cut, so nothing can leak across a cut). + * + * In both modes every copied file gets a fresh row id and storage key, bytes + * are physically copied and counted against the storage quota, and every + * in-transcript file reference is re-pointed at the copies so the new chat + * survives deletion of the source chat. File resources whose chat-owned file + * was NOT copied (a branch fork leaves outputs and post-cut uploads behind) + * are dropped from the new chat's resources rather than left as ghosts + * pointing at the source chat's files. */ export const POST = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ chatId: string }> }) => { @@ -58,12 +70,11 @@ export const POST = withRouteHandler( return createUnauthorizedResponse() } - const parsed = await parseRequest(forkMothershipChatContract, request, context, { - validationErrorResponse: () => createBadRequestResponse('upToMessageId is required'), - }) + const parsed = await parseRequest(forkMothershipChatContract, request, context) if (!parsed.success) return parsed.response const { chatId } = parsed.data.params const { upToMessageId } = parsed.data.body + const isWholeChatDuplicate = !upToMessageId const [parent] = await db .select({ @@ -91,16 +102,22 @@ export const POST = withRouteHandler( } const messages = await loadCopilotChatMessages(chatId) - const forkIdx = messages.findIndex((m) => m.id === upToMessageId) - if (forkIdx < 0) { - return createBadRequestResponse('Message not found in chat') + let forkedMessages = messages + if (upToMessageId) { + const forkIdx = messages.findIndex((m) => m.id === upToMessageId) + if (forkIdx < 0) { + return createBadRequestResponse('Message not found in chat') + } + forkedMessages = messages.slice(0, forkIdx + 1) } - const forkedMessages = messages.slice(0, forkIdx + 1) - // The timeline cut for files: uploads born in any kept message come - // along; uploads born after the fork point stay behind. - const keptMessageIds = new Set(forkedMessages.map((m) => m.id)) - const sourceFiles = await listForkableChatFiles(db, chatId, keptMessageIds) + // The timeline cut for files: a branch copies uploads born in any kept + // message (uploads born after the fork point stay behind); a whole-chat + // duplicate keeps everything, so it copies all uploads AND outputs with + // no cut. + const sourceFiles = isWholeChatDuplicate + ? await listDuplicableChatFiles(db, chatId) + : await listForkableChatFiles(db, chatId, new Set(forkedMessages.map((m) => m.id))) const totalFileBytes = sourceFiles.reduce((sum, row) => sum + row.size, 0) if (totalFileBytes > 0) { const quotaCheck = await checkStorageQuota(userId, totalFileBytes) @@ -109,14 +126,29 @@ export const POST = withRouteHandler( } } - // Resources are stored as a jsonb array on the chat row — copy them directly. + // Resources are stored as a jsonb array on the chat row. They carry no + // timestamps, so they can't be timeline-cut like messages — instead, + // file resources whose chat-owned file is NOT copied (outputs on a + // branch fork, uploads born after the cut) are dropped in the rewrite + // below; everything else is copied. const parentResources = Array.isArray(parent.resources) ? (parent.resources as MothershipResource[]) : [] + // The source chat's chat-owned file ids (uploads + outputs, no cut) — + // the "is this resource a ghost?" test set for the rewrite. On a + // whole-chat duplicate sourceFiles already IS that full set. + const chatOwnedFileIds = new Set( + (isWholeChatDuplicate ? sourceFiles : await listDuplicableChatFiles(db, chatId)).map( + (row) => row.id + ) + ) + const newId = generateId() const baseTitle = (parent.title ?? 'New chat').replace(/^Fork \| /, '') - const title = `Fork | ${baseTitle}` + const title = isWholeChatDuplicate + ? `${parent.title ?? 'New chat'} (Copy)` + : `Fork | ${baseTitle}` const now = new Date() const result = await db.transaction(async (tx) => { @@ -151,10 +183,17 @@ export const POST = withRouteHandler( }) const maps = { fileIds: idMap, fileKeys: keyMap } - if (idMap.size > 0 || keyMap.size > 0) { + const newChatResources = rewriteResourceFileRefs(parentResources, maps, chatOwnedFileIds) + // Skip the redundant update only when the rewrite changed nothing: + // no ids re-pointed AND no ghost resources dropped. + if ( + idMap.size > 0 || + keyMap.size > 0 || + newChatResources.length !== parentResources.length + ) { await tx .update(copilotChats) - .set({ resources: rewriteResourceFileRefs(parentResources, maps) }) + .set({ resources: newChatResources }) .where(eq(copilotChats.id, newId)) } @@ -186,6 +225,8 @@ export const POST = withRouteHandler( } // Clone copilot-service conversation state (messages, active_messages, memory files). + // Omitting upToMessageId selects the service's whole-chat mode, which preserves the + // compacted working memory verbatim instead of rebuilding it from raw messages. // Best-effort: if the copilot service doesn't have a row for the source chat yet, skip. try { const copilotHeaders: Record = { 'Content-Type': 'application/json' } @@ -200,7 +241,7 @@ export const POST = withRouteHandler( body: JSON.stringify({ sourceChatId: chatId, newChatId: newId, - upToMessageId, + ...(upToMessageId ? { upToMessageId } : {}), userId, }), spanName: 'sim → go /api/chats/fork', @@ -227,7 +268,11 @@ export const POST = withRouteHandler( captureServerEvent( userId, 'task_forked', - { workspace_id: parent.workspaceId ?? '', source_chat_id: chatId }, + { + workspace_id: parent.workspaceId ?? '', + source_chat_id: chatId, + whole_chat: isWholeChatDuplicate, + }, { groups: { workspace: parent.workspaceId ?? '' } } ) diff --git a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx index d335ee75d97..4b45030a0a1 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx @@ -17,6 +17,7 @@ import { } from '@sim/emcn' import { GitBranch } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' +import { isLiveAssistantMessageId } from '@/lib/copilot/chat/effective-transcript' import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context' import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback' import { useForkMothershipChat } from '@/hooks/queries/mothership-chats' @@ -162,7 +163,10 @@ export const MessageActions = memo(function MessageActions({ const hasContent = Boolean(content) const canSubmitFeedback = Boolean(chatId && userQuery) - const canFork = Boolean(chatId && messageId) + // A live (just-streamed) assistant message carries a synthetic id that the + // persisted transcript doesn't know — forking it would 400. The button + // appears once the transcript refetch swaps in the persisted message id. + const canFork = Boolean(chatId && messageId && !isLiveAssistantMessageId(messageId)) if (!hasContent && !canSubmitFeedback && !canFork) return null return ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 2e94f37a861..d592c2d2fa4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -92,6 +92,7 @@ import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' import { useDeleteMothershipChat, useDeleteMothershipChats, + useForkMothershipChat, useMarkMothershipChatRead, useMarkMothershipChatUnread, useMothershipChats, @@ -597,6 +598,7 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { const deleteChatMutation = useDeleteMothershipChat(workspaceId) const deleteChatsMutation = useDeleteMothershipChats(workspaceId) + const forkChatMutation = useForkMothershipChat(workspaceId) const markChatReadMutation = useMarkMothershipChatRead(workspaceId) const markChatUnreadMutation = useMarkMothershipChatUnread(workspaceId) const renameChatMutation = useRenameMothershipChat(workspaceId) @@ -963,6 +965,21 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { chatFlyoutRename.startRename({ id: chatId, name: chat.name }) }, [chatFlyoutRename, chats, chatsHover]) + const handleDuplicateChat = useCallback(() => { + const { chatIds: ids } = contextMenuSelectionRef.current + if (ids.length !== 1) return + // No upToMessageId: the fork route treats this as a whole-chat duplicate. + forkChatMutation.mutate( + { chatId: ids[0] }, + { + onSuccess: (result) => { + useFolderStore.getState().clearChatSelection() + navigateToPage(`/workspace/${workspaceId}/chat/${result.id}`) + }, + } + ) + }, [navigateToPage, workspaceId]) + const handleToggleChatPin = useCallback(() => { const { chatIds: ids } = contextMenuSelectionRef.current if (ids.length !== 1) return @@ -1686,6 +1703,7 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { onMarkAsUnread={handleMarkChatAsUnread} onTogglePin={handleToggleChatPin} onRename={handleStartChatRename} + onDuplicate={handleDuplicateChat} onDelete={handleDeleteChat} showOpenInNewTab={!isMultiChatContextMenu} showMarkAsRead={!isMultiChatContextMenu && !!activeChatContextMenuItem?.isUnread} @@ -1697,8 +1715,9 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { showPin={!isMultiChatContextMenu && !!activeChatContextMenuItem} isPinned={!!activeChatContextMenuItem?.isPinned} showRename={!isMultiChatContextMenu} - showDuplicate={false} + showDuplicate={!isMultiChatContextMenu} disableRename={!canEdit} + disableDuplicate={!canEdit || forkChatMutation.isPending} disableDelete={!canEdit} /> diff --git a/apps/sim/hooks/queries/mothership-chats.ts b/apps/sim/hooks/queries/mothership-chats.ts index 10e458c5aef..c23d0d8f073 100644 --- a/apps/sim/hooks/queries/mothership-chats.ts +++ b/apps/sim/hooks/queries/mothership-chats.ts @@ -671,13 +671,17 @@ export function useCreateMothershipChat(workspaceId?: string) { }) } +/** + * With upToMessageId: branch fork, titled "Fork | ". Without it: a + * whole-chat duplicate, titled " (Copy)" — the sidebar Duplicate action. + */ async function forkChat(params: { chatId: string - upToMessageId: string + upToMessageId?: string }): Promise<{ id: string }> { const data = await requestJson(forkMothershipChatContract, { params: { chatId: params.chatId }, - body: { upToMessageId: params.upToMessageId }, + body: params.upToMessageId ? { upToMessageId: params.upToMessageId } : {}, }) return { id: data.id } } @@ -694,10 +698,16 @@ export function useForkMothershipChat(workspaceId?: string) { ) if (existing) { const sourceChat = existing.find((t) => t.id === variables.chatId) - const baseName = (sourceChat?.name ?? 'New chat').replace(/^Fork \| /, '') + const sourceName = sourceChat?.name ?? 'New chat' + const baseName = sourceName.replace(/^Fork \| /, '') + // Mirror the server's title choice so the optimistic row doesn't + // visibly rename itself when the list refetches. + const optimisticName = variables.upToMessageId + ? `Fork | ${baseName}` + : `${sourceName} (Copy)` const optimisticChat: MothershipChatMetadata = { id: data.id, - name: `Fork | ${baseName}`, + name: optimisticName, updatedAt: new Date(), isActive: false, isUnread: false, diff --git a/apps/sim/lib/api/contracts/mothership-chats.ts b/apps/sim/lib/api/contracts/mothership-chats.ts index 7c8cf4f701f..3f9e405909a 100644 --- a/apps/sim/lib/api/contracts/mothership-chats.ts +++ b/apps/sim/lib/api/contracts/mothership-chats.ts @@ -263,7 +263,13 @@ export const deleteMothershipChatContract = defineRouteContract({ }) export const forkMothershipChatBodySchema = z.object({ - upToMessageId: z.string().min(1, 'upToMessageId is required'), + /** + * The fork cut point: messages up to and including this id are kept, and the + * copy is titled "Fork | ". Omitted for a whole-chat duplicate: every + * message is kept, agent outputs/ rows come along, and the copy is titled + * " (Copy)". + */ + upToMessageId: z.string().min(1, 'upToMessageId cannot be empty').optional(), }) export type ForkMothershipChatBody = z.input diff --git a/apps/sim/lib/copilot/chat/effective-transcript.test.ts b/apps/sim/lib/copilot/chat/effective-transcript.test.ts index 285743d37ac..8f69d4fd581 100644 --- a/apps/sim/lib/copilot/chat/effective-transcript.test.ts +++ b/apps/sim/lib/copilot/chat/effective-transcript.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest' import { buildEffectiveChatTranscript, getLiveAssistantMessageId, + isLiveAssistantMessageId, } from '@/lib/copilot/chat/effective-transcript' import { normalizeMessage } from '@/lib/copilot/chat/persisted-message' import { @@ -261,3 +262,11 @@ describe('buildEffectiveChatTranscript', () => { ) }) }) + +describe('isLiveAssistantMessageId', () => { + it('recognizes the synthetic live-assistant id and nothing else', () => { + expect(isLiveAssistantMessageId(getLiveAssistantMessageId('stream-1'))).toBe(true) + expect(isLiveAssistantMessageId('f620fceb-4e9d-4e7f-ab7f-890a2a823564')).toBe(false) + expect(isLiveAssistantMessageId('')).toBe(false) + }) +}) diff --git a/apps/sim/lib/copilot/chat/effective-transcript.ts b/apps/sim/lib/copilot/chat/effective-transcript.ts index ae971047208..8673a6d27dd 100644 --- a/apps/sim/lib/copilot/chat/effective-transcript.ts +++ b/apps/sim/lib/copilot/chat/effective-transcript.ts @@ -34,6 +34,16 @@ export function getLiveAssistantMessageId(streamId: string): string { return `live-assistant:${streamId}` } +/** + * True for the synthetic id of a streaming/just-streamed assistant message. + * These ids exist only in the client's effective transcript — never in the + * persisted one — so message-scoped server actions (e.g. fork) must not be + * offered until the transcript refetch swaps in the persisted message id. + */ +export function isLiveAssistantMessageId(messageId: string): boolean { + return messageId.startsWith('live-assistant:') +} + function asPayloadRecord(value: unknown): Record | undefined { return isRecordLike(value) ? value : undefined } diff --git a/apps/sim/lib/copilot/chat/fork-chat-files.test.ts b/apps/sim/lib/copilot/chat/fork-chat-files.test.ts index c41d52d1b3e..10194aafca0 100644 --- a/apps/sim/lib/copilot/chat/fork-chat-files.test.ts +++ b/apps/sim/lib/copilot/chat/fork-chat-files.test.ts @@ -101,6 +101,39 @@ describe('planChatFileCopies', () => { ]) }) + it('byte-copies output rows the same as uploads (fresh key + blob task) on a duplicate', async () => { + const inserted: Array> = [] + const tx = { + insert: () => ({ + values: async (v: Record) => { + inserted.push(v) + }, + }), + } + + const { idMap, blobTasks } = await planChatFileCopies({ + tx: tx as never, + rows: [makeRow({ id: 'wf_output', context: 'output', messageId: null })], + newChatId: 'chat-copy', + userId: 'user-1', + now: NOW, + }) + + // Live rows can't share a storage key (workspace_files_key_active_unique), + // so outputs get fresh keys and physical byte copies, exactly like uploads. + expect(inserted).toHaveLength(1) + expect(inserted[0].key).toBe('workspace/ws-1/2-cat.png') + expect(inserted[0].context).toBe('output') + expect(idMap.get('wf_output')).toBe(inserted[0].id) + expect(blobTasks).toEqual([ + expect.objectContaining({ + sourceKey: 'workspace/ws-1/1-cat.png', + targetKey: 'workspace/ws-1/2-cat.png', + context: 'output', + }), + ]) + }) + it('skips legacy rows with no workspaceId instead of failing the fork', async () => { const inserted: Array> = [] const tx = { diff --git a/apps/sim/lib/copilot/chat/fork-chat-files.ts b/apps/sim/lib/copilot/chat/fork-chat-files.ts index 548ebc722f4..43fa9ffaa2c 100644 --- a/apps/sim/lib/copilot/chat/fork-chat-files.ts +++ b/apps/sim/lib/copilot/chat/fork-chat-files.ts @@ -13,7 +13,7 @@ import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' const logger = createLogger('ForkChatFiles') /** - * The only chat-owned storage context a fork copies: user uploads + * The only chat-owned storage context a branch fork copies: user uploads * (`mothership`). Agent-generated `outputs/` rows deliberately stay behind — a * fork starts with an empty outputs/ namespace. Shared workspace `files/` * (`context='workspace'`) is workspace-owned, not chat-owned — both chats @@ -21,6 +21,16 @@ const logger = createLogger('ForkChatFiles') */ export const FORKABLE_CHAT_FILE_CONTEXT: StorageContext = 'mothership' +/** + * The chat-owned contexts a whole-chat duplicate copies: user uploads + * (`mothership`) AND agent-generated outputs (`output`). A duplicate is a + * self-contained snapshot, so outputs come along — bytes included (every + * copied row gets a fresh storage key; live rows can't share a key because of + * the `workspace_files_key_active_unique` index, and serve/view lookups + * resolve by key). Workspace `files/` stays referenced in place, as above. + */ +export const DUPLICABLE_CHAT_FILE_CONTEXTS: readonly StorageContext[] = ['mothership', 'output'] + export type ForkableChatFileRow = typeof workspaceFiles.$inferSelect /** One blob byte-copy to run after the fork transaction commits. */ @@ -73,7 +83,29 @@ export async function listForkableChatFiles( } /** - * Insert copy rows for the kept upload files under the new chat id (fresh + * The live file rows a whole-chat duplicate copies: every upload AND output + * owned by the chat, no timeline cut — nothing is being left behind, so + * nothing needs a birthdate. Also used pre-transaction to sum sizes for the + * storage-quota gate. + */ +export async function listDuplicableChatFiles( + db: DbOrTx, + chatId: string +): Promise { + return db + .select() + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.chatId, chatId), + inArray(workspaceFiles.context, [...DUPLICABLE_CHAT_FILE_CONTEXTS]), + isNull(workspaceFiles.deletedAt) + ) + ) +} + +/** + * Insert copy rows for the kept chat-owned files under the new chat id (fresh * `wf_` id + fresh storage key; `message_id` carries over verbatim so the copy * matches the same message in the forked transcript; display names carry over * verbatim because their uniqueness is per-chat and the new chat is an empty diff --git a/apps/sim/lib/copilot/chat/rewrite-file-references.test.ts b/apps/sim/lib/copilot/chat/rewrite-file-references.test.ts index ad8e148102d..8206a5af894 100644 --- a/apps/sim/lib/copilot/chat/rewrite-file-references.test.ts +++ b/apps/sim/lib/copilot/chat/rewrite-file-references.test.ts @@ -122,4 +122,38 @@ describe('rewriteResourceFileRefs', () => { const resources: MothershipResource[] = [{ type: 'file', id: OLD_ID, title: 'cat.png' }] expect(rewriteResourceFileRefs(resources, emptyMaps)).toBe(resources) }) + + it('drops ghost file resources: chat-owned but not copied (e.g. outputs on a branch fork)', () => { + const resources: MothershipResource[] = [ + { type: 'file', id: OLD_ID, title: 'apple-upload.png' }, + { type: 'file', id: 'wf_banana_output', title: 'banana.png' }, + { type: 'file', id: 'wf_shared_workspace', title: 'shared.pdf' }, + { type: 'workflow', id: 'wflow-1', title: 'My flow' }, + ] + // Owned by the source chat: the copied upload and the uncopied output. + // The shared workspace file is not chat-owned and must pass through. + const owned = new Set([OLD_ID, 'wf_banana_output']) + + const result = rewriteResourceFileRefs(resources, maps, owned) + + expect(result).toEqual([ + { type: 'file', id: NEW_ID, title: 'apple-upload.png' }, + { type: 'file', id: 'wf_shared_workspace', title: 'shared.pdf' }, + { type: 'workflow', id: 'wflow-1', title: 'My flow' }, + ]) + }) + + it('drops ghosts even when nothing was copied (empty maps + drop set)', () => { + const resources: MothershipResource[] = [ + { type: 'file', id: 'wf_banana_output', title: 'banana.png' }, + { type: 'table', id: 'tbl-1', title: 'Orders' }, + ] + const result = rewriteResourceFileRefs(resources, emptyMaps, new Set(['wf_banana_output'])) + expect(result).toEqual([{ type: 'table', id: 'tbl-1', title: 'Orders' }]) + }) + + it('keeps everything when the drop set is empty', () => { + const resources: MothershipResource[] = [{ type: 'file', id: OLD_ID, title: 'cat.png' }] + expect(rewriteResourceFileRefs(resources, emptyMaps, new Set())).toBe(resources) + }) }) diff --git a/apps/sim/lib/copilot/chat/rewrite-file-references.ts b/apps/sim/lib/copilot/chat/rewrite-file-references.ts index 78882fe973a..2147e5f0995 100644 --- a/apps/sim/lib/copilot/chat/rewrite-file-references.ts +++ b/apps/sim/lib/copilot/chat/rewrite-file-references.ts @@ -66,15 +66,25 @@ export function rewriteMessageFileRefs( * stores raw `workspace_files.id`s) at the copied files. Non-file resources * (workflows, tables, knowledge bases…) reference shared workspace entities * and pass through unchanged. + * + * `dropFileIds` is the source chat's chat-owned file ids (uploads + outputs). + * A file resource pointing at one of these that was NOT copied is a ghost in + * the new chat — its file stays behind on a branch fork (outputs always, + * uploads born after the cut) — so it is dropped rather than left pointing at + * the source chat's file. Shared workspace files are not chat-owned, never + * appear in the set, and pass through unchanged. */ export function rewriteResourceFileRefs( resources: MothershipResource[], - maps: ChatFileRefMaps + maps: ChatFileRefMaps, + dropFileIds?: ReadonlySet ): MothershipResource[] { - if (!hasMappings(maps)) return resources - return resources.map((resource) => - resource.type === 'file' - ? { ...resource, id: maps.fileIds.get(resource.id) ?? resource.id } - : resource - ) + if (!hasMappings(maps) && !dropFileIds?.size) return resources + return resources.flatMap((resource) => { + if (resource.type !== 'file') return [resource] + const copyId = maps.fileIds.get(resource.id) + if (copyId) return [{ ...resource, id: copyId }] + if (dropFileIds?.has(resource.id)) return [] + return [resource] + }) } diff --git a/apps/sim/lib/copilot/tools/handlers/output-file-reader.ts b/apps/sim/lib/copilot/tools/handlers/output-file-reader.ts index dfd8ae9e44f..e332eac5b92 100644 --- a/apps/sim/lib/copilot/tools/handlers/output-file-reader.ts +++ b/apps/sim/lib/copilot/tools/handlers/output-file-reader.ts @@ -111,6 +111,19 @@ export async function findChatOutputRowByChatAndName( return allRows.find((r) => canonicalOutputKey(vfsName(r)) === segmentKey) ?? null } +/** + * Resolve a chat output by VFS name to a serve-ready {@link WorkspaceFileRecord} + * (storageContext `output`), for callers that need the file itself rather than + * its text content (e.g. media tools loading a reference image). + */ +export async function resolveChatOutputRecord( + chatId: string, + fileName: string +): Promise { + const row = await findChatOutputRowByChatAndName(chatId, fileName) + return row ? toWorkspaceFileRecord(row) : null +} + /** * List all chat-scoped outputs for a given chat in creation order. */ diff --git a/apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts b/apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts index 0e914229c8a..e25b257b059 100644 --- a/apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts +++ b/apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts @@ -112,6 +112,19 @@ export async function findMothershipUploadRowByChatAndName( return allRows.find((r) => canonicalUploadKey(vfsName(r)) === segmentKey) ?? null } +/** + * Resolve a chat upload by VFS name to a serve-ready {@link WorkspaceFileRecord} + * (storageContext `mothership`), for callers that need the file itself rather + * than its text content (e.g. media tools loading a reference image). + */ +export async function resolveChatUploadRecord( + chatId: string, + fileName: string +): Promise { + const row = await findMothershipUploadRowByChatAndName(chatId, fileName) + return row ? toWorkspaceFileRecord(row) : null +} + /** * List all chat-scoped uploads for a given chat in upload order. */ diff --git a/apps/sim/lib/copilot/tools/server/files/resolve-input-file.test.ts b/apps/sim/lib/copilot/tools/server/files/resolve-input-file.test.ts new file mode 100644 index 00000000000..5a3d78cd89a --- /dev/null +++ b/apps/sim/lib/copilot/tools/server/files/resolve-input-file.test.ts @@ -0,0 +1,90 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockResolveChatUploadRecord, mockResolveChatOutputRecord, mockResolveWorkspaceFileRef } = + vi.hoisted(() => ({ + mockResolveChatUploadRecord: vi.fn(), + mockResolveChatOutputRecord: vi.fn(), + mockResolveWorkspaceFileRef: vi.fn(), + })) + +vi.mock('@/lib/copilot/tools/handlers/upload-file-reader', () => ({ + resolveChatUploadRecord: mockResolveChatUploadRecord, +})) + +vi.mock('@/lib/copilot/tools/handlers/output-file-reader', () => ({ + resolveChatOutputRecord: mockResolveChatOutputRecord, +})) + +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({ + resolveWorkspaceFileReference: mockResolveWorkspaceFileRef, +})) + +import { resolveToolInputFile } from '@/lib/copilot/tools/server/files/resolve-input-file' + +const UPLOAD_RECORD = { id: 'wf_upload', name: 'ref.jpg', storageContext: 'mothership' } +const OUTPUT_RECORD = { id: 'wf_output', name: 'gen.png', storageContext: 'output' } +const WORKSPACE_RECORD = { id: 'wf_shared', name: 'shared.pdf' } + +describe('resolveToolInputFile', () => { + beforeEach(() => { + vi.clearAllMocks() + mockResolveChatUploadRecord.mockResolvedValue(UPLOAD_RECORD) + mockResolveChatOutputRecord.mockResolvedValue(OUTPUT_RECORD) + mockResolveWorkspaceFileRef.mockResolvedValue(WORKSPACE_RECORD) + }) + + it('resolves uploads/ through the chat upload resolver', async () => { + const record = await resolveToolInputFile({ + workspaceId: 'ws-1', + chatId: 'chat-1', + path: 'uploads/ref.jpg', + }) + expect(record).toBe(UPLOAD_RECORD) + expect(mockResolveChatUploadRecord).toHaveBeenCalledWith('chat-1', 'ref.jpg') + expect(mockResolveWorkspaceFileRef).not.toHaveBeenCalled() + }) + + it('resolves outputs/ through the chat output resolver', async () => { + const record = await resolveToolInputFile({ + workspaceId: 'ws-1', + chatId: 'chat-1', + path: 'outputs/gen.png', + }) + expect(record).toBe(OUTPUT_RECORD) + expect(mockResolveChatOutputRecord).toHaveBeenCalledWith('chat-1', 'gen.png') + }) + + it('ignores a stray trailing segment on the flat chat namespaces', async () => { + await resolveToolInputFile({ + workspaceId: 'ws-1', + chatId: 'chat-1', + path: 'uploads/ref.jpg/content', + }) + expect(mockResolveChatUploadRecord).toHaveBeenCalledWith('chat-1', 'ref.jpg') + }) + + it('returns null for chat-scoped paths without a chat', async () => { + const upload = await resolveToolInputFile({ workspaceId: 'ws-1', path: 'uploads/ref.jpg' }) + const output = await resolveToolInputFile({ workspaceId: 'ws-1', path: 'outputs/gen.png' }) + expect(upload).toBeNull() + expect(output).toBeNull() + expect(mockResolveChatUploadRecord).not.toHaveBeenCalled() + expect(mockResolveChatOutputRecord).not.toHaveBeenCalled() + }) + + it('falls back to the workspace resolver for files/ paths and wf_ ids', async () => { + const byPath = await resolveToolInputFile({ + workspaceId: 'ws-1', + chatId: 'chat-1', + path: 'files/shared.pdf', + }) + const byId = await resolveToolInputFile({ workspaceId: 'ws-1', path: 'wf_shared' }) + expect(byPath).toBe(WORKSPACE_RECORD) + expect(byId).toBe(WORKSPACE_RECORD) + expect(mockResolveWorkspaceFileRef).toHaveBeenCalledWith('ws-1', 'files/shared.pdf') + expect(mockResolveWorkspaceFileRef).toHaveBeenCalledWith('ws-1', 'wf_shared') + }) +}) diff --git a/apps/sim/lib/copilot/tools/server/files/resolve-input-file.ts b/apps/sim/lib/copilot/tools/server/files/resolve-input-file.ts new file mode 100644 index 00000000000..94595c64dd1 --- /dev/null +++ b/apps/sim/lib/copilot/tools/server/files/resolve-input-file.ts @@ -0,0 +1,34 @@ +import { resolveChatOutputRecord } from '@/lib/copilot/tools/handlers/output-file-reader' +import { resolveChatUploadRecord } from '@/lib/copilot/tools/handlers/upload-file-reader' +import { + resolveWorkspaceFileReference, + type WorkspaceFileRecord, +} from '@/lib/uploads/contexts/workspace/workspace-file-manager' + +/** + * Resolve a tool input file path across every VFS namespace the agent can + * reference: chat-scoped `uploads/` and `outputs/` (resolved by + * chat + VFS name, raw or percent-encoded), and workspace `files/` paths / + * `wf_` ids (delegated to {@link resolveWorkspaceFileReference}, which only + * knows `context='workspace'` rows — the reason chat-scoped inputs need this + * wrapper). Chat-scoped prefixes resolve to null without a chatId; uploads + * are flat, so any trailing segment after the name is ignored. + */ +export async function resolveToolInputFile(params: { + workspaceId: string + chatId?: string + path: string +}): Promise { + const { workspaceId, chatId, path } = params + if (path.startsWith('uploads/')) { + if (!chatId) return null + const fileName = path.slice('uploads/'.length).split('/')[0] + return resolveChatUploadRecord(chatId, fileName) + } + if (path.startsWith('outputs/')) { + if (!chatId) return null + const fileName = path.slice('outputs/'.length).split('/')[0] + return resolveChatOutputRecord(chatId, fileName) + } + return resolveWorkspaceFileReference(workspaceId, path) +} diff --git a/apps/sim/lib/copilot/tools/server/image/generate-image.ts b/apps/sim/lib/copilot/tools/server/image/generate-image.ts index 7cb18f16c4b..bdf2cfe00f5 100644 --- a/apps/sim/lib/copilot/tools/server/image/generate-image.ts +++ b/apps/sim/lib/copilot/tools/server/image/generate-image.ts @@ -7,12 +7,10 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' +import { resolveToolInputFile } from '@/lib/copilot/tools/server/files/resolve-input-file' import { writeWorkspaceFileByPath } from '@/lib/copilot/vfs/resource-writer' import { getRotatingApiKey } from '@/lib/core/config/api-keys' -import { - fetchWorkspaceFileBuffer, - resolveWorkspaceFileReference, -} from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { fetchWorkspaceFileBuffer } from '@/lib/uploads/contexts/workspace/workspace-file-manager' const logger = createLogger('GenerateImageTool') @@ -86,30 +84,44 @@ export const generateImageServerTool: BaseServerTool> + let buffer: Buffer try { - const fileRecord = await resolveWorkspaceFileReference(workspaceId, filePath) - if (fileRecord) { - const buffer = await fetchWorkspaceFileBuffer(fileRecord) - const base64 = buffer.toString('base64') - const mime = fileRecord.type || 'image/png' - parts.push({ - inlineData: { mimeType: mime, data: base64 }, - }) - logger.info('Loaded reference image', { - filePath, - name: fileRecord.name, - size: buffer.length, - mimeType: mime, - }) - } else { - logger.warn('Reference file not found, skipping', { filePath }) + fileRecord = await resolveToolInputFile({ + workspaceId, + chatId: context.chatId, + path: filePath, + }) + if (!fileRecord) { + return { + success: false, + message: withMessageId( + `Reference file not found: "${filePath}". Check the path (files/, uploads/, or outputs/) and try again.` + ), + } } + buffer = await fetchWorkspaceFileBuffer(fileRecord) } catch (err) { - logger.warn('Failed to load reference image, skipping', { - filePath, - error: toError(err).message, - }) + return { + success: false, + message: withMessageId( + `Failed to load reference image "${filePath}": ${toError(err).message}` + ), + } } + const mime = fileRecord.type || 'image/png' + parts.push({ + inlineData: { mimeType: mime, data: buffer.toString('base64') }, + }) + logger.info('Loaded reference image', { + filePath, + name: fileRecord.name, + size: buffer.length, + mimeType: mime, + }) } } diff --git a/apps/sim/lib/copilot/tools/server/media/ffmpeg.ts b/apps/sim/lib/copilot/tools/server/media/ffmpeg.ts index a402f8d26e8..1561f8efeb8 100644 --- a/apps/sim/lib/copilot/tools/server/media/ffmpeg.ts +++ b/apps/sim/lib/copilot/tools/server/media/ffmpeg.ts @@ -6,12 +6,10 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' +import { resolveToolInputFile } from '@/lib/copilot/tools/server/files/resolve-input-file' import { writeWorkspaceFileByPath } from '@/lib/copilot/vfs/resource-writer' import { type FfmpegOperation, type MediaFile, runFfmpegOperation } from '@/lib/media/ffmpeg' -import { - fetchWorkspaceFileBuffer, - resolveWorkspaceFileReference, -} from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { fetchWorkspaceFileBuffer } from '@/lib/uploads/contexts/workspace/workspace-file-manager' const logger = createLogger('FfmpegTool') @@ -83,7 +81,11 @@ export const ffmpegServerTool: BaseServerTool = { try { const mediaFiles: MediaFile[] = [] for (const filePath of inputPaths) { - const fileRecord = await resolveWorkspaceFileReference(workspaceId, filePath) + const fileRecord = await resolveToolInputFile({ + workspaceId, + chatId: context.chatId, + path: filePath, + }) if (!fileRecord) { return { success: false, message: `Input file not found: ${filePath}` } } diff --git a/apps/sim/lib/copilot/tools/server/media/generate-audio.ts b/apps/sim/lib/copilot/tools/server/media/generate-audio.ts index dcd498519d4..6a15417061a 100644 --- a/apps/sim/lib/copilot/tools/server/media/generate-audio.ts +++ b/apps/sim/lib/copilot/tools/server/media/generate-audio.ts @@ -6,12 +6,10 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' +import { resolveToolInputFile } from '@/lib/copilot/tools/server/files/resolve-input-file' import { writeWorkspaceFileByPath } from '@/lib/copilot/vfs/resource-writer' import { type AudioType, generateFalAudio } from '@/lib/media/falai-audio' -import { - fetchWorkspaceFileBuffer, - resolveWorkspaceFileReference, -} from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { fetchWorkspaceFileBuffer } from '@/lib/uploads/contexts/workspace/workspace-file-manager' const logger = createLogger('GenerateAudioTool') @@ -84,7 +82,11 @@ export const generateAudioServerTool: BaseServerTool Date: Fri, 3 Jul 2026 16:31:08 -0700 Subject: [PATCH 07/10] fix(chat): delete dead file rows when a fork blob copy fails --- .../chats/[chatId]/fork/route.test.ts | 55 ++++++++++++++++++- .../mothership/chats/[chatId]/fork/route.ts | 32 +++++++++-- .../workspaces/[id]/files/[fileId]/route.ts | 15 ++--- .../message-actions/message-actions.tsx | 5 ++ .../[workspaceId]/home/hooks/use-chat.ts | 6 ++ .../w/components/sidebar/sidebar.tsx | 6 ++ apps/sim/hooks/queries/mothership-chats.ts | 4 +- apps/sim/hooks/queries/workspace-files.ts | 19 ++++--- .../sim/lib/api/contracts/mothership-chats.ts | 7 +++ apps/sim/lib/api/contracts/workspace-files.ts | 12 ++++ .../lib/copilot/chat/fork-chat-files.test.ts | 20 ++++--- apps/sim/lib/copilot/chat/fork-chat-files.ts | 14 ++++- .../tools/handlers/materialize-file.ts | 5 +- 13 files changed, 165 insertions(+), 35 deletions(-) diff --git a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts index 40d3325899f..4b738a8e25c 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.test.ts @@ -19,6 +19,8 @@ const { mockFetchGo, mockPublishStatusChanged, mockCaptureServerEvent, + mockDeleteWhere, + mockRemoveChatResources, } = vi.hoisted(() => ({ mockTransaction: vi.fn(), mockSelectRows: vi.fn(), @@ -33,6 +35,8 @@ const { mockFetchGo: vi.fn(), mockPublishStatusChanged: vi.fn(), mockCaptureServerEvent: vi.fn(), + mockDeleteWhere: vi.fn(), + mockRemoveChatResources: vi.fn(), })) vi.mock('@sim/db', () => ({ @@ -44,6 +48,9 @@ vi.mock('@sim/db', () => ({ }), }), }), + delete: () => ({ + where: mockDeleteWhere, + }), transaction: mockTransaction, }, })) @@ -61,10 +68,18 @@ vi.mock('@sim/db/schema', () => ({ planArtifact: 'copilotChats.planArtifact', config: 'copilotChats.config', }, + workspaceFiles: { + id: 'workspaceFiles.id', + }, })) vi.mock('drizzle-orm', () => ({ eq: vi.fn((field: unknown, value: unknown) => ({ type: 'eq', field, value })), + inArray: vi.fn((field: unknown, values: unknown) => ({ type: 'inArray', field, values })), +})) + +vi.mock('@/lib/copilot/resources/persistence', () => ({ + removeChatResources: mockRemoveChatResources, })) vi.mock('@/lib/copilot/request/http', () => copilotHttpMock) @@ -206,8 +221,10 @@ describe('POST /api/mothership/chats/[chatId]/fork', () => { keyMap: new Map(), blobTasks: [], }) - mockExecuteChatFileBlobCopies.mockResolvedValue({ copied: 0, failed: 0 }) + mockExecuteChatFileBlobCopies.mockResolvedValue({ copied: 0, failed: 0, failedCopyIds: [] }) mockAppendCopilotChatMessages.mockResolvedValue(undefined) + mockDeleteWhere.mockResolvedValue(undefined) + mockRemoveChatResources.mockResolvedValue(undefined) mockAssertActiveWorkspaceAccess.mockResolvedValue(undefined) mockFetchGo.mockResolvedValue({ ok: true }) mockTransaction.mockImplementation(async (cb: (tx: unknown) => Promise) => @@ -352,6 +369,42 @@ describe('POST /api/mothership/chats/[chatId]/fork', () => { expect(res.status).toBe(200) }) + it('surfaces failed blob copies and cleans up their dead rows + resource chips', async () => { + mockExecuteChatFileBlobCopies.mockResolvedValue({ + copied: 1, + failed: 2, + failedCopyIds: ['wf_dead1', 'wf_dead2'], + }) + + const failedRes = await POST(createRequest('chat-1'), makeContext('chat-1')) + const body = await failedRes.json() + + expect(body.failedFileCopies).toBe(2) + + // The dead rows (committed, but no bytes behind them) are hard-deleted so + // they vanish from VFS listings and name resolution… + expect(mockDeleteWhere).toHaveBeenCalledWith({ + type: 'inArray', + field: 'workspaceFiles.id', + values: ['wf_dead1', 'wf_dead2'], + }) + // …and their resource chips are dropped from the new chat. + expect(mockRemoveChatResources).toHaveBeenCalledWith(body.id, [ + { type: 'file', id: 'wf_dead1', title: '' }, + { type: 'file', id: 'wf_dead2', title: '' }, + ]) + }) + + it('omits failedFileCopies and skips cleanup when every blob copies', async () => { + mockExecuteChatFileBlobCopies.mockResolvedValue({ copied: 3, failed: 0, failedCopyIds: [] }) + + const cleanRes = await POST(createRequest('chat-1'), makeContext('chat-1')) + + expect('failedFileCopies' in (await cleanRes.json())).toBe(false) + expect(mockDeleteWhere).not.toHaveBeenCalled() + expect(mockRemoveChatResources).not.toHaveBeenCalled() + }) + it('drops ghost resources on a branch fork: chat-owned files that were not copied', async () => { // The source chat generated two outputs (apple pre-cut, banana post-cut) // and has one upload + one shared workspace-file resource. A branch fork diff --git a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts index 148293121ea..c7749fe271b 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts @@ -1,8 +1,8 @@ import { db } from '@sim/db' -import { copilotChats } from '@sim/db/schema' +import { copilotChats, workspaceFiles } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import { eq } from 'drizzle-orm' +import { eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { forkMothershipChatContract } from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' @@ -29,6 +29,7 @@ import { createNotFoundResponse, createUnauthorizedResponse, } from '@/lib/copilot/request/http' +import { removeChatResources } from '@/lib/copilot/resources/persistence' import type { MothershipResource } from '@/lib/copilot/resources/types' import { getMothershipBaseURL, getMothershipSourceEnvHeaders } from '@/lib/copilot/server/agent-url' import { env } from '@/lib/core/config/env' @@ -211,11 +212,30 @@ export const POST = withRouteHandler( } const newChat = result.row - const { copied, failed } = await executeChatFileBlobCopies(result.blobTasks, { + const { copied, failed, failedCopyIds } = await executeChatFileBlobCopies(result.blobTasks, { userId, workspaceId: parent.workspaceId ?? undefined, }) if (failed > 0) { + // A failed blob copy leaves a committed row with no bytes behind it. + // Cleanly absent beats present-but-broken: hard-delete the dead rows + // (they vanish from the VFS listings and name resolution) and drop + // their resource chips from the new chat. Inline transcript embeds + // can't be healed — those 404 — which is what `failedFileCopies` in + // the response warns the user about. + try { + await db.delete(workspaceFiles).where(inArray(workspaceFiles.id, failedCopyIds)) + await removeChatResources( + newId, + failedCopyIds.map((id) => ({ type: 'file' as const, id, title: '' })) + ) + } catch (cleanupError) { + logger.error('Failed to clean up dead file rows after blob-copy failure', { + newChatId: newId, + failedCopyIds, + error: cleanupError, + }) + } logger.warn('Some chat file blobs failed to copy during fork', { chatId, newChatId: newId, @@ -276,7 +296,11 @@ export const POST = withRouteHandler( { groups: { workspace: parent.workspaceId ?? '' } } ) - return NextResponse.json({ success: true, id: newId }) + return NextResponse.json({ + success: true, + id: newId, + ...(failed > 0 ? { failedFileCopies: failed } : {}), + }) } catch (error) { if (isWorkspaceAccessDeniedError(error)) { return createForbiddenResponse('Workspace access denied') diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts index 62e337048f2..3e50da6a7fc 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { + getWorkspaceFileByIdContract, renameWorkspaceFileContract, workspaceFileParamsSchema, } from '@/lib/api/contracts/workspace-files' @@ -28,16 +29,8 @@ const logger = createLogger('WorkspaceFileAPI') * the list-based lookup can't see. Requires workspace membership (read). */ export const GET = withRouteHandler( - async (request: NextRequest, { params }: { params: Promise<{ id: string; fileId: string }> }) => { + async (request: NextRequest, context: { params: Promise<{ id: string; fileId: string }> }) => { const requestId = generateRequestId() - const paramsResult = workspaceFileParamsSchema.safeParse(await params) - if (!paramsResult.success) { - return NextResponse.json( - { error: getValidationErrorMessage(paramsResult.error, 'Invalid route parameters') }, - { status: 400 } - ) - } - const { id: workspaceId, fileId } = paramsResult.data try { const session = await getSession() @@ -45,6 +38,10 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } + const parsed = await parseRequest(getWorkspaceFileByIdContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId, fileId } = parsed.data.params + const userPermission = await getUserEntityPermissions( session.user.id, 'workspace', diff --git a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx index 4b45030a0a1..36664e8c0ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx @@ -154,6 +154,11 @@ export const MessageActions = memo(function MessageActions({ if (!chatId || !messageId || forkChat.isPending) return try { const result = await forkChat.mutateAsync({ chatId, upToMessageId: messageId }) + if (result.failedFileCopies) { + toast.warning( + `${result.failedFileCopies} file${result.failedFileCopies === 1 ? '' : 's'} could not be copied to the fork` + ) + } useFolderStore.getState().clearChatSelection() router.push(`/workspace/${params.workspaceId}/chat/${result.id}`) } catch { diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index b9bc03cca38..ec5c9006ef6 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -3320,6 +3320,12 @@ export function useChat( ...(streamTargetChatId ? { targetChatId: streamTargetChatId } : {}), }) if (succeeded) return consumedByTranscript + // The 409 means THIS send was never accepted, and the failed + // reconnect means the conflicting stream is gone too — undo the + // optimistic message and phantom activeStreamId (mirrors the + // generic pre-stream failure path) instead of leaving a stuck + // in-flight turn. + rollbackOptimisticSend() if (streamGenRef.current === gen) { finalize({ error: true, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index d592c2d2fa4..bdc522a5236 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -17,6 +17,7 @@ import { Loader, Skeleton, Tooltip, + toast, Upload, } from '@sim/emcn' import { @@ -973,6 +974,11 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { { chatId: ids[0] }, { onSuccess: (result) => { + if (result.failedFileCopies) { + toast.warning( + `${result.failedFileCopies} file${result.failedFileCopies === 1 ? '' : 's'} could not be copied to the duplicate` + ) + } useFolderStore.getState().clearChatSelection() navigateToPage(`/workspace/${workspaceId}/chat/${result.id}`) }, diff --git a/apps/sim/hooks/queries/mothership-chats.ts b/apps/sim/hooks/queries/mothership-chats.ts index c23d0d8f073..4996a8c3d13 100644 --- a/apps/sim/hooks/queries/mothership-chats.ts +++ b/apps/sim/hooks/queries/mothership-chats.ts @@ -678,12 +678,12 @@ export function useCreateMothershipChat(workspaceId?: string) { async function forkChat(params: { chatId: string upToMessageId?: string -}): Promise<{ id: string }> { +}): Promise<{ id: string; failedFileCopies?: number }> { const data = await requestJson(forkMothershipChatContract, { params: { chatId: params.chatId }, body: params.upToMessageId ? { upToMessageId: params.upToMessageId } : {}, }) - return { id: data.id } + return { id: data.id, failedFileCopies: data.failedFileCopies } } export function useForkMothershipChat(workspaceId?: string) { diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index aeb1d89d87e..42c780a7754 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -10,6 +10,7 @@ import { listChatOutputsContract } from '@/lib/api/contracts/mothership-chats' import { getUsageLimitsContract } from '@/lib/api/contracts/usage-limits' import { deleteWorkspaceFileContract, + getWorkspaceFileByIdContract, listWorkspaceFilesContract, registerWorkspaceFileContract, renameWorkspaceFileContract, @@ -120,14 +121,16 @@ export function useWorkspaceFileById(workspaceId: string, fileId: string, enable return useQuery({ queryKey: [...workspaceFilesKeys.all, 'byId', workspaceId, fileId] as const, queryFn: async ({ signal }): Promise => { - // boundary-raw-fetch: single-file record lookup incl. chat-scoped outputs; no contract - const response = await fetch( - `/api/workspaces/${encodeURIComponent(workspaceId)}/files/${encodeURIComponent(fileId)}`, - { signal, cache: 'no-store' } - ) - if (!response.ok) return null - const data = await response.json().catch(() => null) - return data?.success && data.file ? (data.file as WorkspaceFileRecord) : null + try { + const data = await requestJson(getWorkspaceFileByIdContract, { + params: { id: workspaceId, fileId }, + signal, + }) + return data.success ? data.file : null + } catch { + // 404 (deleted / not previewable) resolves to null — the panel treats it as a miss. + return null + } }, enabled: enabled && !!workspaceId && !!fileId, staleTime: 30 * 1000, diff --git a/apps/sim/lib/api/contracts/mothership-chats.ts b/apps/sim/lib/api/contracts/mothership-chats.ts index 3f9e405909a..8138a791ce1 100644 --- a/apps/sim/lib/api/contracts/mothership-chats.ts +++ b/apps/sim/lib/api/contracts/mothership-chats.ts @@ -283,6 +283,13 @@ export const forkMothershipChatContract = defineRouteContract({ schema: z.object({ success: z.literal(true), id: z.string(), + /** + * Present (and > 0) when some file blobs could not be byte-copied: the + * new chat exists and its transcript references those copies, but their + * bytes are missing (blob copies are best-effort, post-transaction). + * Callers should surface a warning. + */ + failedFileCopies: z.number().optional(), }), }, }) diff --git a/apps/sim/lib/api/contracts/workspace-files.ts b/apps/sim/lib/api/contracts/workspace-files.ts index fdf933e96f2..bce9baca710 100644 --- a/apps/sim/lib/api/contracts/workspace-files.ts +++ b/apps/sim/lib/api/contracts/workspace-files.ts @@ -90,6 +90,18 @@ export const listWorkspaceFilesContract = defineRouteContract({ }, }) +export const getWorkspaceFileByIdContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/files/[fileId]', + params: workspaceFileParamsSchema, + response: { + mode: 'json', + schema: workspaceFileSuccessSchema.extend({ + file: workspaceFileRecordSchema, + }), + }, +}) + export const renameWorkspaceFileContract = defineRouteContract({ method: 'PATCH', path: '/api/workspaces/[id]/files/[fileId]', diff --git a/apps/sim/lib/copilot/chat/fork-chat-files.test.ts b/apps/sim/lib/copilot/chat/fork-chat-files.test.ts index 10194aafca0..4b0c1a3e29b 100644 --- a/apps/sim/lib/copilot/chat/fork-chat-files.test.ts +++ b/apps/sim/lib/copilot/chat/fork-chat-files.test.ts @@ -92,6 +92,7 @@ describe('planChatFileCopies', () => { expect(keyMap.get('workspace/ws-1/1-cat.png')).toBe('workspace/ws-1/2-cat.png') expect(blobTasks).toEqual([ { + copyId: copy.id, sourceKey: 'workspace/ws-1/1-cat.png', targetKey: 'workspace/ws-1/2-cat.png', context: 'mothership', @@ -160,6 +161,7 @@ describe('planChatFileCopies', () => { describe('executeChatFileBlobCopies', () => { const task = { + copyId: 'wf_copy', sourceKey: 'workspace/ws-1/1-cat.png', targetKey: 'workspace/ws-1/2-cat.png', context: 'mothership' as const, @@ -180,7 +182,7 @@ describe('executeChatFileBlobCopies', () => { workspaceId: 'ws-1', }) - expect(result).toEqual({ copied: 1, failed: 0 }) + expect(result).toEqual({ copied: 1, failed: 0, failedCopyIds: [] }) expect(mockUploadFile).toHaveBeenCalledWith( expect.objectContaining({ customKey: 'workspace/ws-1/2-cat.png', @@ -192,15 +194,19 @@ describe('executeChatFileBlobCopies', () => { expect(mockIncrementStorageUsage).toHaveBeenCalledWith('user-1', 10, 'ws-1') }) - it('is best-effort: a failed download skips the file and counts nothing', async () => { + it('is best-effort: a failed download skips the file, counts nothing, and reports its copy id', async () => { mockDownloadFile.mockRejectedValueOnce(new Error('blob missing')) - const result = await executeChatFileBlobCopies([task, task], { - userId: 'user-1', - workspaceId: 'ws-1', - }) + const result = await executeChatFileBlobCopies( + [task, { ...task, copyId: 'wf_copy2', targetKey: 'workspace/ws-1/3-cat.png' }], + { + userId: 'user-1', + workspaceId: 'ws-1', + } + ) - expect(result).toEqual({ copied: 1, failed: 1 }) + // The first task's download failed — its copy id comes back for row cleanup. + expect(result).toEqual({ copied: 1, failed: 1, failedCopyIds: ['wf_copy'] }) expect(mockIncrementStorageUsage).toHaveBeenCalledTimes(1) }) }) diff --git a/apps/sim/lib/copilot/chat/fork-chat-files.ts b/apps/sim/lib/copilot/chat/fork-chat-files.ts index 43fa9ffaa2c..611bfd47c1b 100644 --- a/apps/sim/lib/copilot/chat/fork-chat-files.ts +++ b/apps/sim/lib/copilot/chat/fork-chat-files.ts @@ -35,6 +35,8 @@ export type ForkableChatFileRow = typeof workspaceFiles.$inferSelect /** One blob byte-copy to run after the fork transaction commits. */ export interface ChatBlobCopyTask { + /** The copied `workspace_files` row's id — used to delete the row if the blob copy fails. */ + copyId: string sourceKey: string targetKey: string context: StorageContext @@ -147,6 +149,7 @@ export async function planChatFileCopies(params: { idMap.set(row.id, copyId) keyMap.set(row.key, targetKey) blobTasks.push({ + copyId, sourceKey: row.key, targetKey, context: row.context as StorageContext, @@ -162,14 +165,18 @@ export async function planChatFileCopies(params: { * Copy each planned blob to its new key, best-effort: a failed copy logs a * warning and is skipped (the fork keeps its transcript; that one file is * missing) rather than failing the whole fork. Each successfully copied file - * increments the storage-usage counter by its actual byte length. + * increments the storage-usage counter by its actual byte length. Failed + * tasks' copy-row ids are returned so the caller can delete the dead rows + * (row exists, blob doesn't) instead of leaving them listed in the VFS and + * resources with nothing behind them. */ export async function executeChatFileBlobCopies( blobTasks: ChatBlobCopyTask[], params: { userId: string; workspaceId?: string } -): Promise<{ copied: number; failed: number }> { +): Promise<{ copied: number; failed: number; failedCopyIds: string[] }> { let copied = 0 let failed = 0 + const failedCopyIds: string[] = [] for (const task of blobTasks) { try { const buffer = await downloadFile({ @@ -205,6 +212,7 @@ export async function executeChatFileBlobCopies( } } catch (error) { failed += 1 + failedCopyIds.push(task.copyId) logger.warn('Failed to copy chat file blob during fork', { sourceKey: task.sourceKey, targetKey: task.targetKey, @@ -212,5 +220,5 @@ export async function executeChatFileBlobCopies( }) } } - return { copied, failed } + return { copied, failed, failedCopyIds } } diff --git a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts index 5c9c9139e1d..c5971b06494 100644 --- a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts +++ b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts @@ -78,9 +78,12 @@ async function executeSave(fileName: string, chatId: string): Promise Date: Fri, 3 Jul 2026 16:40:57 -0700 Subject: [PATCH 08/10] chore(db): biome-format drizzle artifacts for migration 0254 --- .../db/migrations/meta/0254_snapshot.json | 1665 ++++------------- packages/db/migrations/meta/_journal.json | 2 +- 2 files changed, 401 insertions(+), 1266 deletions(-) diff --git a/packages/db/migrations/meta/0254_snapshot.json b/packages/db/migrations/meta/0254_snapshot.json index f2ae314cdf9..cce4d7bda91 100644 --- a/packages/db/migrations/meta/0254_snapshot.json +++ b/packages/db/migrations/meta/0254_snapshot.json @@ -155,12 +155,8 @@ "name": "academy_certificate_user_id_user_id_fk", "tableFrom": "academy_certificate", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -170,9 +166,7 @@ "academy_certificate_certificate_number_unique": { "name": "academy_certificate_certificate_number_unique", "nullsNotDistinct": false, - "columns": [ - "certificate_number" - ] + "columns": ["certificate_number"] } }, "policies": {}, @@ -305,12 +299,8 @@ "name": "account_user_id_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -465,12 +455,8 @@ "name": "api_key_user_id_user_id_fk", "tableFrom": "api_key", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -478,12 +464,8 @@ "name": "api_key_workspace_id_workspace_id_fk", "tableFrom": "api_key", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -491,12 +473,8 @@ "name": "api_key_created_by_user_id_fk", "tableFrom": "api_key", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -506,9 +484,7 @@ "api_key_key_unique": { "name": "api_key_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -919,12 +895,8 @@ "name": "audit_log_workspace_id_workspace_id_fk", "tableFrom": "audit_log", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -932,12 +904,8 @@ "name": "audit_log_actor_id_user_id_fk", "tableFrom": "audit_log", "tableTo": "user", - "columnsFrom": [ - "actor_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1072,12 +1040,8 @@ "name": "background_work_status_workspace_id_workspace_id_fk", "tableFrom": "background_work_status", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1085,12 +1049,8 @@ "name": "background_work_status_workflow_id_workflow_id_fk", "tableFrom": "background_work_status", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1263,12 +1223,8 @@ "name": "chat_workflow_id_workflow_id_fk", "tableFrom": "chat", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1276,12 +1232,8 @@ "name": "chat_user_id_user_id_fk", "tableFrom": "chat", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1490,12 +1442,8 @@ "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", "tableFrom": "copilot_async_tool_calls", "tableTo": "copilot_runs", - "columnsFrom": [ - "run_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["run_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1503,12 +1451,8 @@ "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", "tableFrom": "copilot_async_tool_calls", "tableTo": "copilot_run_checkpoints", - "columnsFrom": [ - "checkpoint_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1764,12 +1708,8 @@ "name": "copilot_chats_user_id_user_id_fk", "tableFrom": "copilot_chats", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1777,12 +1717,8 @@ "name": "copilot_chats_workflow_id_workflow_id_fk", "tableFrom": "copilot_chats", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1790,12 +1726,8 @@ "name": "copilot_chats_workspace_id_workspace_id_fk", "tableFrom": "copilot_chats", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1962,12 +1894,8 @@ "name": "copilot_feedback_user_id_user_id_fk", "tableFrom": "copilot_feedback", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1975,12 +1903,8 @@ "name": "copilot_feedback_chat_id_copilot_chats_id_fk", "tableFrom": "copilot_feedback", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2183,12 +2107,8 @@ "name": "copilot_messages_chat_id_copilot_chats_id_fk", "tableFrom": "copilot_messages", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2316,12 +2236,8 @@ "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", "tableFrom": "copilot_run_checkpoints", "tableTo": "copilot_runs", - "columnsFrom": [ - "run_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["run_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2648,12 +2564,8 @@ "name": "copilot_runs_chat_id_copilot_chats_id_fk", "tableFrom": "copilot_runs", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2661,12 +2573,8 @@ "name": "copilot_runs_user_id_user_id_fk", "tableFrom": "copilot_runs", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2674,12 +2582,8 @@ "name": "copilot_runs_workflow_id_workflow_id_fk", "tableFrom": "copilot_runs", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2687,12 +2591,8 @@ "name": "copilot_runs_workspace_id_workspace_id_fk", "tableFrom": "copilot_runs", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2805,12 +2705,8 @@ "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", "tableFrom": "copilot_workflow_read_hashes", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2818,12 +2714,8 @@ "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", "tableFrom": "copilot_workflow_read_hashes", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3086,12 +2978,8 @@ "name": "credential_workspace_id_workspace_id_fk", "tableFrom": "credential", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3099,12 +2987,8 @@ "name": "credential_account_id_account_id_fk", "tableFrom": "credential", "tableTo": "account", - "columnsFrom": [ - "account_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["account_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3112,12 +2996,8 @@ "name": "credential_env_owner_user_id_user_id_fk", "tableFrom": "credential", "tableTo": "user", - "columnsFrom": [ - "env_owner_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3125,12 +3005,8 @@ "name": "credential_created_by_user_id_fk", "tableFrom": "credential", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3292,12 +3168,8 @@ "name": "credential_member_credential_id_credential_id_fk", "tableFrom": "credential_member", "tableTo": "credential", - "columnsFrom": [ - "credential_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3305,12 +3177,8 @@ "name": "credential_member_user_id_user_id_fk", "tableFrom": "credential_member", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3318,12 +3186,8 @@ "name": "credential_member_invited_by_user_id_fk", "tableFrom": "credential_member", "tableTo": "user", - "columnsFrom": [ - "invited_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3447,12 +3311,8 @@ "name": "credential_set_organization_id_organization_id_fk", "tableFrom": "credential_set", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3460,12 +3320,8 @@ "name": "credential_set_created_by_user_id_fk", "tableFrom": "credential_set", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3611,12 +3467,8 @@ "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", "tableFrom": "credential_set_invitation", "tableTo": "credential_set", - "columnsFrom": [ - "credential_set_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3624,12 +3476,8 @@ "name": "credential_set_invitation_invited_by_user_id_fk", "tableFrom": "credential_set_invitation", "tableTo": "user", - "columnsFrom": [ - "invited_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3637,12 +3485,8 @@ "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", "tableFrom": "credential_set_invitation", "tableTo": "user", - "columnsFrom": [ - "accepted_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3652,9 +3496,7 @@ "credential_set_invitation_token_unique": { "name": "credential_set_invitation_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -3776,12 +3618,8 @@ "name": "credential_set_member_credential_set_id_credential_set_id_fk", "tableFrom": "credential_set_member", "tableTo": "credential_set", - "columnsFrom": [ - "credential_set_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3789,12 +3627,8 @@ "name": "credential_set_member_user_id_user_id_fk", "tableFrom": "credential_set_member", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3802,12 +3636,8 @@ "name": "credential_set_member_invited_by_user_id_fk", "tableFrom": "credential_set_member", "tableTo": "user", - "columnsFrom": [ - "invited_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3916,12 +3746,8 @@ "name": "custom_tools_workspace_id_workspace_id_fk", "tableFrom": "custom_tools", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3929,12 +3755,8 @@ "name": "custom_tools_user_id_user_id_fk", "tableFrom": "custom_tools", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -4056,12 +3878,8 @@ "name": "data_drain_runs_drain_id_data_drains_id_fk", "tableFrom": "data_drain_runs", "tableTo": "data_drains", - "columnsFrom": [ - "drain_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4237,12 +4055,8 @@ "name": "data_drains_organization_id_organization_id_fk", "tableFrom": "data_drains", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4250,12 +4064,8 @@ "name": "data_drains_created_by_user_id_fk", "tableFrom": "data_drains", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -5162,12 +4972,8 @@ "name": "document_knowledge_base_id_knowledge_base_id_fk", "tableFrom": "document", "tableTo": "knowledge_base", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5175,12 +4981,8 @@ "name": "document_connector_id_knowledge_connector_id_fk", "tableFrom": "document", "tableTo": "knowledge_connector", - "columnsFrom": [ - "connector_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -5188,12 +4990,8 @@ "name": "document_uploaded_by_user_id_fk", "tableFrom": "document", "tableTo": "user", - "columnsFrom": [ - "uploaded_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -5825,12 +5623,8 @@ "name": "embedding_knowledge_base_id_knowledge_base_id_fk", "tableFrom": "embedding", "tableTo": "knowledge_base", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5838,12 +5632,8 @@ "name": "embedding_document_id_document_id_fk", "tableFrom": "embedding", "tableTo": "document", - "columnsFrom": [ - "document_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["document_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5895,12 +5685,8 @@ "name": "environment_user_id_user_id_fk", "tableFrom": "environment", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5910,9 +5696,7 @@ "environment_user_id_unique": { "name": "environment_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -5998,12 +5782,8 @@ "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", "tableFrom": "execution_large_value_dependencies", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6011,10 +5791,7 @@ "compositePrimaryKeys": { "execution_large_value_dependencies_parent_key_child_key_pk": { "name": "execution_large_value_dependencies_parent_key_child_key_pk", - "columns": [ - "parent_key", - "child_key" - ] + "columns": ["parent_key", "child_key"] } }, "uniqueConstraints": {}, @@ -6099,12 +5876,8 @@ "name": "execution_large_value_references_workspace_id_workspace_id_fk", "tableFrom": "execution_large_value_references", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6112,12 +5885,8 @@ "name": "execution_large_value_references_workflow_id_workflow_id_fk", "tableFrom": "execution_large_value_references", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6125,11 +5894,7 @@ "compositePrimaryKeys": { "execution_large_value_references_key_execution_id_source_pk": { "name": "execution_large_value_references_key_execution_id_source_pk", - "columns": [ - "key", - "execution_id", - "source" - ] + "columns": ["key", "execution_id", "source"] } }, "uniqueConstraints": {}, @@ -6263,12 +6028,8 @@ "name": "execution_large_values_workspace_id_workspace_id_fk", "tableFrom": "execution_large_values", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6276,12 +6037,8 @@ "name": "execution_large_values_workflow_id_workflow_id_fk", "tableFrom": "execution_large_values", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6499,12 +6256,8 @@ "name": "invitation_inviter_id_user_id_fk", "tableFrom": "invitation", "tableTo": "user", - "columnsFrom": [ - "inviter_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6512,12 +6265,8 @@ "name": "invitation_organization_id_organization_id_fk", "tableFrom": "invitation", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6527,9 +6276,7 @@ "invitation_token_unique": { "name": "invitation_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -6623,12 +6370,8 @@ "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", "tableFrom": "invitation_workspace_grant", "tableTo": "invitation", - "columnsFrom": [ - "invitation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6636,12 +6379,8 @@ "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", "tableFrom": "invitation_workspace_grant", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6838,12 +6577,8 @@ "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", "tableFrom": "job_execution_logs", "tableTo": "workflow_schedule", - "columnsFrom": [ - "schedule_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -6851,12 +6586,8 @@ "name": "job_execution_logs_workspace_id_workspace_id_fk", "tableFrom": "job_execution_logs", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7067,12 +6798,8 @@ "name": "knowledge_base_user_id_user_id_fk", "tableFrom": "knowledge_base", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7080,12 +6807,8 @@ "name": "knowledge_base_workspace_id_workspace_id_fk", "tableFrom": "knowledge_base", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7210,12 +6933,8 @@ "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", "tableFrom": "knowledge_base_tag_definitions", "tableTo": "knowledge_base", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7420,12 +7139,8 @@ "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", "tableFrom": "knowledge_connector", "tableTo": "knowledge_base", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7535,12 +7250,8 @@ "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", "tableFrom": "knowledge_connector_sync_log", "tableTo": "knowledge_connector", - "columnsFrom": [ - "connector_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7667,12 +7378,8 @@ "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", "tableFrom": "mcp_server_oauth", "tableTo": "mcp_servers", - "columnsFrom": [ - "mcp_server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7680,12 +7387,8 @@ "name": "mcp_server_oauth_user_id_user_id_fk", "tableFrom": "mcp_server_oauth", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -7693,12 +7396,8 @@ "name": "mcp_server_oauth_workspace_id_workspace_id_fk", "tableFrom": "mcp_server_oauth", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7925,12 +7624,8 @@ "name": "mcp_servers_workspace_id_workspace_id_fk", "tableFrom": "mcp_servers", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7938,12 +7633,8 @@ "name": "mcp_servers_created_by_user_id_fk", "tableFrom": "mcp_servers", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -8027,12 +7718,8 @@ "name": "member_user_id_user_id_fk", "tableFrom": "member", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8040,12 +7727,8 @@ "name": "member_organization_id_organization_id_fk", "tableFrom": "member", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8185,12 +7868,8 @@ "name": "memory_workspace_id_workspace_id_fk", "tableFrom": "memory", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8271,12 +7950,8 @@ "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", "tableFrom": "mothership_inbox_allowed_sender", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8284,12 +7959,8 @@ "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", "tableFrom": "mothership_inbox_allowed_sender", "tableTo": "user", - "columnsFrom": [ - "added_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["added_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8525,12 +8196,8 @@ "name": "mothership_inbox_task_workspace_id_workspace_id_fk", "tableFrom": "mothership_inbox_task", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8538,12 +8205,8 @@ "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", "tableFrom": "mothership_inbox_task", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -8596,12 +8259,8 @@ "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", "tableFrom": "mothership_inbox_webhook", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8611,9 +8270,7 @@ "mothership_inbox_webhook_workspace_id_unique": { "name": "mothership_inbox_webhook_workspace_id_unique", "nullsNotDistinct": false, - "columns": [ - "workspace_id" - ] + "columns": ["workspace_id"] } }, "policies": {}, @@ -8688,12 +8345,8 @@ "name": "mothership_settings_workspace_id_workspace_id_fk", "tableFrom": "mothership_settings", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8899,12 +8552,8 @@ "name": "organization_member_usage_limit_organization_id_organization_id_fk", "tableFrom": "organization_member_usage_limit", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8912,12 +8561,8 @@ "name": "organization_member_usage_limit_user_id_user_id_fk", "tableFrom": "organization_member_usage_limit", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8925,12 +8570,8 @@ "name": "organization_member_usage_limit_set_by_user_id_fk", "tableFrom": "organization_member_usage_limit", "tableTo": "user", - "columnsFrom": [ - "set_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["set_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -9218,12 +8859,8 @@ "name": "paused_executions_workflow_id_workflow_id_fk", "tableFrom": "paused_executions", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9328,12 +8965,8 @@ "name": "pending_credential_draft_user_id_user_id_fk", "tableFrom": "pending_credential_draft", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9341,12 +8974,8 @@ "name": "pending_credential_draft_workspace_id_workspace_id_fk", "tableFrom": "pending_credential_draft", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9354,12 +8983,8 @@ "name": "pending_credential_draft_credential_id_credential_id_fk", "tableFrom": "pending_credential_draft", "tableTo": "credential", - "columnsFrom": [ - "credential_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9492,12 +9117,8 @@ "name": "permission_group_organization_id_organization_id_fk", "tableFrom": "permission_group", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9505,12 +9126,8 @@ "name": "permission_group_created_by_user_id_fk", "tableFrom": "permission_group", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9627,12 +9244,8 @@ "name": "permission_group_member_permission_group_id_permission_group_id_fk", "tableFrom": "permission_group_member", "tableTo": "permission_group", - "columnsFrom": [ - "permission_group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9640,12 +9253,8 @@ "name": "permission_group_member_organization_id_organization_id_fk", "tableFrom": "permission_group_member", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9653,12 +9262,8 @@ "name": "permission_group_member_user_id_user_id_fk", "tableFrom": "permission_group_member", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9666,12 +9271,8 @@ "name": "permission_group_member_assigned_by_user_id_fk", "tableFrom": "permission_group_member", "tableTo": "user", - "columnsFrom": [ - "assigned_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -9761,12 +9362,8 @@ "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", "tableFrom": "permission_group_workspace", "tableTo": "permission_group", - "columnsFrom": [ - "permission_group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9774,12 +9371,8 @@ "name": "permission_group_workspace_workspace_id_workspace_id_fk", "tableFrom": "permission_group_workspace", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9787,12 +9380,8 @@ "name": "permission_group_workspace_organization_id_organization_id_fk", "tableFrom": "permission_group_workspace", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9998,12 +9587,8 @@ "name": "permissions_user_id_user_id_fk", "tableFrom": "permissions", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10169,12 +9754,8 @@ "name": "public_share_workspace_id_workspace_id_fk", "tableFrom": "public_share", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10182,12 +9763,8 @@ "name": "public_share_created_by_user_id_fk", "tableFrom": "public_share", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -10358,12 +9935,8 @@ "name": "resume_queue_paused_execution_id_paused_executions_id_fk", "tableFrom": "resume_queue", "tableTo": "paused_executions", - "columnsFrom": [ - "paused_execution_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10476,12 +10049,8 @@ "name": "session_user_id_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10489,12 +10058,8 @@ "name": "session_active_organization_id_organization_id_fk", "tableFrom": "session", "tableTo": "organization", - "columnsFrom": [ - "active_organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -10504,9 +10069,7 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -10646,12 +10209,8 @@ "name": "settings_user_id_user_id_fk", "tableFrom": "settings", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10661,9 +10220,7 @@ "settings_user_id_unique": { "name": "settings_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -10713,12 +10270,8 @@ "name": "sim_trigger_state_workflow_id_workflow_id_fk", "tableFrom": "sim_trigger_state", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10726,11 +10279,7 @@ "compositePrimaryKeys": { "sim_trigger_state_workflow_id_block_id_scope_key_pk": { "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", - "columns": [ - "workflow_id", - "block_id", - "scope_key" - ] + "columns": ["workflow_id", "block_id", "scope_key"] } }, "uniqueConstraints": {}, @@ -10821,12 +10370,8 @@ "name": "skill_workspace_id_workspace_id_fk", "tableFrom": "skill", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10834,12 +10379,8 @@ "name": "skill_user_id_user_id_fk", "tableFrom": "skill", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -10970,12 +10511,8 @@ "name": "sso_provider_user_id_user_id_fk", "tableFrom": "sso_provider", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10983,12 +10520,8 @@ "name": "sso_provider_organization_id_organization_id_fk", "tableFrom": "sso_provider", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11287,12 +10820,8 @@ "name": "table_jobs_table_id_user_table_definitions_id_fk", "tableFrom": "table_jobs", "tableTo": "user_table_definitions", - "columnsFrom": [ - "table_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["table_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11300,12 +10829,8 @@ "name": "table_jobs_workspace_id_workspace_id_fk", "tableFrom": "table_jobs", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11468,12 +10993,8 @@ "name": "table_row_executions_table_id_user_table_definitions_id_fk", "tableFrom": "table_row_executions", "tableTo": "user_table_definitions", - "columnsFrom": [ - "table_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["table_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11481,12 +11002,8 @@ "name": "table_row_executions_row_id_user_table_rows_id_fk", "tableFrom": "table_row_executions", "tableTo": "user_table_rows", - "columnsFrom": [ - "row_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["row_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11494,10 +11011,7 @@ "compositePrimaryKeys": { "table_row_executions_row_id_group_id_pk": { "name": "table_row_executions_row_id_group_id_pk", - "columns": [ - "row_id", - "group_id" - ] + "columns": ["row_id", "group_id"] } }, "uniqueConstraints": {}, @@ -11654,12 +11168,8 @@ "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", "tableFrom": "table_run_dispatches", "tableTo": "user_table_definitions", - "columnsFrom": [ - "table_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["table_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11667,12 +11177,8 @@ "name": "table_run_dispatches_workspace_id_workspace_id_fk", "tableFrom": "table_run_dispatches", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11680,12 +11186,8 @@ "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", "tableFrom": "table_run_dispatches", "tableTo": "user", - "columnsFrom": [ - "triggered_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -11960,12 +11462,8 @@ "name": "usage_log_user_id_user_id_fk", "tableFrom": "usage_log", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11973,12 +11471,8 @@ "name": "usage_log_workspace_id_workspace_id_fk", "tableFrom": "usage_log", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -11986,12 +11480,8 @@ "name": "usage_log_workflow_id_workflow_id_fk", "tableFrom": "usage_log", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -12099,16 +11589,12 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] }, "user_normalized_email_unique": { "name": "user_normalized_email_unique", "nullsNotDistinct": false, - "columns": [ - "normalized_email" - ] + "columns": ["normalized_email"] } }, "policies": {}, @@ -12340,12 +11826,8 @@ "name": "user_stats_user_id_user_id_fk", "tableFrom": "user_stats", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -12355,9 +11837,7 @@ "user_stats_user_id_unique": { "name": "user_stats_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -12533,12 +12013,8 @@ "name": "user_table_definitions_workspace_id_workspace_id_fk", "tableFrom": "user_table_definitions", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12546,12 +12022,8 @@ "name": "user_table_definitions_created_by_user_id_fk", "tableFrom": "user_table_definitions", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -12742,12 +12214,8 @@ "name": "user_table_rows_table_id_user_table_definitions_id_fk", "tableFrom": "user_table_rows", "tableTo": "user_table_definitions", - "columnsFrom": [ - "table_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["table_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12755,12 +12223,8 @@ "name": "user_table_rows_workspace_id_workspace_id_fk", "tableFrom": "user_table_rows", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12768,12 +12232,8 @@ "name": "user_table_rows_created_by_user_id_fk", "tableFrom": "user_table_rows", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -12909,9 +12369,7 @@ "waitlist_email_unique": { "name": "waitlist_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -13152,12 +12610,8 @@ "name": "webhook_workflow_id_workflow_id_fk", "tableFrom": "webhook", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13165,12 +12619,8 @@ "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", "tableFrom": "webhook", "tableTo": "workflow_deployment_version", - "columnsFrom": [ - "deployment_version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13178,12 +12628,8 @@ "name": "webhook_credential_set_id_credential_set_id_fk", "tableFrom": "webhook", "tableTo": "credential_set", - "columnsFrom": [ - "credential_set_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -13457,12 +12903,8 @@ "name": "workflow_user_id_user_id_fk", "tableFrom": "workflow", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13470,12 +12912,8 @@ "name": "workflow_workspace_id_workspace_id_fk", "tableFrom": "workflow", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13483,12 +12921,8 @@ "name": "workflow_folder_id_workflow_folder_id_fk", "tableFrom": "workflow", "tableTo": "workflow_folder", - "columnsFrom": [ - "folder_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -13661,12 +13095,8 @@ "name": "workflow_blocks_workflow_id_workflow_id_fk", "tableFrom": "workflow_blocks", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13878,12 +13308,8 @@ "name": "workflow_checkpoints_user_id_user_id_fk", "tableFrom": "workflow_checkpoints", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13891,12 +13317,8 @@ "name": "workflow_checkpoints_workflow_id_workflow_id_fk", "tableFrom": "workflow_checkpoints", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13904,12 +13326,8 @@ "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", "tableFrom": "workflow_checkpoints", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14045,12 +13463,8 @@ "name": "workflow_deployment_version_workflow_id_workflow_id_fk", "tableFrom": "workflow_deployment_version", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14173,12 +13587,8 @@ "name": "workflow_edges_workflow_id_workflow_id_fk", "tableFrom": "workflow_edges", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14186,12 +13596,8 @@ "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", "tableFrom": "workflow_edges", "tableTo": "workflow_blocks", - "columnsFrom": [ - "source_block_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14199,12 +13605,8 @@ "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", "tableFrom": "workflow_edges", "tableTo": "workflow_blocks", - "columnsFrom": [ - "target_block_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14591,12 +13993,8 @@ "name": "workflow_execution_logs_workflow_id_workflow_id_fk", "tableFrom": "workflow_execution_logs", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -14604,12 +14002,8 @@ "name": "workflow_execution_logs_workspace_id_workspace_id_fk", "tableFrom": "workflow_execution_logs", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14617,12 +14011,8 @@ "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", "tableFrom": "workflow_execution_logs", "tableTo": "workflow_execution_snapshots", - "columnsFrom": [ - "state_snapshot_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -14630,12 +14020,8 @@ "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", "tableFrom": "workflow_execution_logs", "tableTo": "workflow_deployment_version", - "columnsFrom": [ - "deployment_version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -14755,12 +14141,8 @@ "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", "tableFrom": "workflow_execution_snapshots", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -14955,12 +14337,8 @@ "name": "workflow_folder_user_id_user_id_fk", "tableFrom": "workflow_folder", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14968,12 +14346,8 @@ "name": "workflow_folder_workspace_id_workspace_id_fk", "tableFrom": "workflow_folder", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15120,12 +14494,8 @@ "name": "workflow_mcp_server_workspace_id_workspace_id_fk", "tableFrom": "workflow_mcp_server", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15133,12 +14503,8 @@ "name": "workflow_mcp_server_created_by_user_id_fk", "tableFrom": "workflow_mcp_server", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15293,12 +14659,8 @@ "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", "tableFrom": "workflow_mcp_tool", "tableTo": "workflow_mcp_server", - "columnsFrom": [ - "server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15306,12 +14668,8 @@ "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", "tableFrom": "workflow_mcp_tool", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15689,12 +15047,8 @@ "name": "workflow_schedule_workflow_id_workflow_id_fk", "tableFrom": "workflow_schedule", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15702,12 +15056,8 @@ "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", "tableFrom": "workflow_schedule", "tableTo": "workflow_deployment_version", - "columnsFrom": [ - "deployment_version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15715,12 +15065,8 @@ "name": "workflow_schedule_source_user_id_user_id_fk", "tableFrom": "workflow_schedule", "tableTo": "user", - "columnsFrom": [ - "source_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15728,12 +15074,8 @@ "name": "workflow_schedule_source_workspace_id_workspace_id_fk", "tableFrom": "workflow_schedule", "tableTo": "workspace", - "columnsFrom": [ - "source_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15831,12 +15173,8 @@ "name": "workflow_subflows_workflow_id_workflow_id_fk", "tableFrom": "workflow_subflows", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16022,12 +15360,8 @@ "name": "workspace_owner_id_user_id_fk", "tableFrom": "workspace", "tableTo": "user", - "columnsFrom": [ - "owner_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16035,12 +15369,8 @@ "name": "workspace_organization_id_organization_id_fk", "tableFrom": "workspace", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -16048,12 +15378,8 @@ "name": "workspace_billed_account_user_id_user_id_fk", "tableFrom": "workspace", "tableTo": "user", - "columnsFrom": [ - "billed_account_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -16061,12 +15387,8 @@ "name": "workspace_forked_from_workspace_id_workspace_id_fk", "tableFrom": "workspace", "tableTo": "workspace", - "columnsFrom": [ - "forked_from_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -16160,12 +15482,8 @@ "name": "workspace_byok_keys_workspace_id_workspace_id_fk", "tableFrom": "workspace_byok_keys", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16173,12 +15491,8 @@ "name": "workspace_byok_keys_created_by_user_id_fk", "tableFrom": "workspace_byok_keys", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -16249,12 +15563,8 @@ "name": "workspace_environment_workspace_id_workspace_id_fk", "tableFrom": "workspace_environment", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16399,12 +15709,8 @@ "name": "workspace_file_workspace_id_workspace_id_fk", "tableFrom": "workspace_file", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16412,12 +15718,8 @@ "name": "workspace_file_uploaded_by_user_id_fk", "tableFrom": "workspace_file", "tableTo": "user", - "columnsFrom": [ - "uploaded_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16427,9 +15729,7 @@ "workspace_file_key_unique": { "name": "workspace_file_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -16612,12 +15912,8 @@ "name": "workspace_file_folders_user_id_user_id_fk", "tableFrom": "workspace_file_folders", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16625,12 +15921,8 @@ "name": "workspace_file_folders_workspace_id_workspace_id_fk", "tableFrom": "workspace_file_folders", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16638,12 +15930,8 @@ "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", "tableFrom": "workspace_file_folders", "tableTo": "workspace_file_folders", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -16951,12 +16239,8 @@ "name": "workspace_files_user_id_user_id_fk", "tableFrom": "workspace_files", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16964,12 +16248,8 @@ "name": "workspace_files_workspace_id_workspace_id_fk", "tableFrom": "workspace_files", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16977,12 +16257,8 @@ "name": "workspace_files_folder_id_workspace_file_folders_id_fk", "tableFrom": "workspace_files", "tableTo": "workspace_file_folders", - "columnsFrom": [ - "folder_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -16990,12 +16266,8 @@ "name": "workspace_files_chat_id_copilot_chats_id_fk", "tableFrom": "workspace_files", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17152,12 +16424,8 @@ "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", "tableFrom": "workspace_fork_block_map", "tableTo": "workspace", - "columnsFrom": [ - "child_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17284,12 +16552,8 @@ "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", "tableFrom": "workspace_fork_dependent_value", "tableTo": "workspace", - "columnsFrom": [ - "child_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17398,12 +16662,8 @@ "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", "tableFrom": "workspace_fork_promote_run", "tableTo": "workspace", - "columnsFrom": [ - "child_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17411,12 +16671,8 @@ "name": "workspace_fork_promote_run_created_by_user_id_fk", "tableFrom": "workspace_fork_promote_run", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -17553,12 +16809,8 @@ "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", "tableFrom": "workspace_fork_resource_map", "tableTo": "workspace", - "columnsFrom": [ - "child_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17566,12 +16818,8 @@ "name": "workspace_fork_resource_map_created_by_user_id_fk", "tableFrom": "workspace_fork_resource_map", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -17587,228 +16835,122 @@ "public.academy_cert_status": { "name": "academy_cert_status", "schema": "public", - "values": [ - "active", - "revoked", - "expired" - ] + "values": ["active", "revoked", "expired"] }, "public.background_work_kind": { "name": "background_work_kind", "schema": "public", - "values": [ - "deployment_side_effects", - "fork_content_copy", - "fork_sync", - "fork_rollback" - ] + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] }, "public.background_work_status_value": { "name": "background_work_status_value", "schema": "public", - "values": [ - "pending", - "processing", - "completed", - "completed_with_warnings", - "failed" - ] + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] }, "public.billing_blocked_reason": { "name": "billing_blocked_reason", "schema": "public", - "values": [ - "payment_failed", - "dispute" - ] + "values": ["payment_failed", "dispute"] }, "public.billing_entity_type": { "name": "billing_entity_type", "schema": "public", - "values": [ - "user", - "organization" - ] + "values": ["user", "organization"] }, "public.chat_type": { "name": "chat_type", "schema": "public", - "values": [ - "mothership", - "copilot" - ] + "values": ["mothership", "copilot"] }, "public.copilot_async_tool_status": { "name": "copilot_async_tool_status", "schema": "public", - "values": [ - "pending", - "running", - "completed", - "failed", - "cancelled", - "delivered" - ] + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] }, "public.copilot_run_status": { "name": "copilot_run_status", "schema": "public", - "values": [ - "active", - "paused_waiting_for_tool", - "resuming", - "complete", - "error", - "cancelled" - ] + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] }, "public.credential_member_role": { "name": "credential_member_role", "schema": "public", - "values": [ - "admin", - "member" - ] + "values": ["admin", "member"] }, "public.credential_member_status": { "name": "credential_member_status", "schema": "public", - "values": [ - "active", - "pending", - "revoked" - ] + "values": ["active", "pending", "revoked"] }, "public.credential_set_invitation_status": { "name": "credential_set_invitation_status", "schema": "public", - "values": [ - "pending", - "accepted", - "expired", - "cancelled" - ] + "values": ["pending", "accepted", "expired", "cancelled"] }, "public.credential_set_member_status": { "name": "credential_set_member_status", "schema": "public", - "values": [ - "active", - "pending", - "revoked" - ] + "values": ["active", "pending", "revoked"] }, "public.credential_type": { "name": "credential_type", "schema": "public", - "values": [ - "oauth", - "env_workspace", - "env_personal", - "service_account" - ] + "values": ["oauth", "env_workspace", "env_personal", "service_account"] }, "public.data_drain_cadence": { "name": "data_drain_cadence", "schema": "public", - "values": [ - "hourly", - "daily" - ] + "values": ["hourly", "daily"] }, "public.data_drain_destination": { "name": "data_drain_destination", "schema": "public", - "values": [ - "s3", - "gcs", - "azure_blob", - "datadog", - "bigquery", - "snowflake", - "webhook" - ] + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] }, "public.data_drain_run_status": { "name": "data_drain_run_status", "schema": "public", - "values": [ - "running", - "success", - "failed" - ] + "values": ["running", "success", "failed"] }, "public.data_drain_run_trigger": { "name": "data_drain_run_trigger", "schema": "public", - "values": [ - "cron", - "manual" - ] + "values": ["cron", "manual"] }, "public.data_drain_source": { "name": "data_drain_source", "schema": "public", - "values": [ - "workflow_logs", - "job_logs", - "audit_logs", - "copilot_chats", - "copilot_runs" - ] + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] }, "public.execution_large_value_reference_source": { "name": "execution_large_value_reference_source", "schema": "public", - "values": [ - "execution_log", - "paused_snapshot" - ] + "values": ["execution_log", "paused_snapshot"] }, "public.invitation_kind": { "name": "invitation_kind", "schema": "public", - "values": [ - "organization", - "workspace" - ] + "values": ["organization", "workspace"] }, "public.invitation_membership_intent": { "name": "invitation_membership_intent", "schema": "public", - "values": [ - "internal", - "external" - ] + "values": ["internal", "external"] }, "public.invitation_status": { "name": "invitation_status", "schema": "public", - "values": [ - "pending", - "accepted", - "rejected", - "cancelled", - "expired" - ] + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] }, "public.permission_type": { "name": "permission_type", "schema": "public", - "values": [ - "admin", - "write", - "read" - ] + "values": ["admin", "write", "read"] }, "public.usage_log_category": { "name": "usage_log_category", "schema": "public", - "values": [ - "model", - "fixed", - "tool" - ] + "values": ["model", "fixed", "tool"] }, "public.usage_log_source": { "name": "usage_log_source", @@ -17828,10 +16970,7 @@ "public.workspace_fork_promote_direction": { "name": "workspace_fork_promote_direction", "schema": "public", - "values": [ - "push", - "pull" - ] + "values": ["push", "pull"] }, "public.workspace_fork_resource_type": { "name": "workspace_fork_resource_type", @@ -17853,11 +16992,7 @@ "public.workspace_mode": { "name": "workspace_mode", "schema": "public", - "values": [ - "personal", - "organization", - "grandfathered_shared" - ] + "values": ["personal", "organization", "grandfathered_shared"] } }, "schemas": {}, @@ -17870,4 +17005,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 55c100fe938..46b32accd98 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1781,4 +1781,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} From 9d8756c416f02a64be2519fe5a2da43e6ca10824 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:54:38 -0700 Subject: [PATCH 09/10] fix(files): require chat ownership for output file access; duplicate error toast --- apps/sim/app/api/files/authorization.test.ts | 66 +++++++++++++++++++ apps/sim/app/api/files/authorization.ts | 56 ++++++++++++++-- .../workspaces/[id]/files/[fileId]/route.ts | 2 +- .../w/components/sidebar/sidebar.tsx | 4 ++ .../workspace/workspace-file-manager.ts | 17 ++++- 5 files changed, 134 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/api/files/authorization.test.ts b/apps/sim/app/api/files/authorization.test.ts index 4409108eacd..054243b6b65 100644 --- a/apps/sim/app/api/files/authorization.test.ts +++ b/apps/sim/app/api/files/authorization.test.ts @@ -210,3 +210,69 @@ describe('public-context access (profile-pictures / og-images / workspace-logos) expect(mockGetUserEntityPermissions).not.toHaveBeenCalled() }) }) + +/** + * Chat-scoped `output` files belong to a private chat: workspace membership + * alone must NOT grant access — the caller must also be the row's owner. + * These lock in the fix for the member-reads-another-member's-outputs leak, + * on both the explicit-context path (view route) and the inferred-workspace + * path (serve route — output keys share the workspace key shape). + */ +describe('chat output file ownership (verifyFileAccess)', () => { + const OUTPUT_KEY = 'workspace/ws-1/1780000000-abcd-generated-image.png' + const outputRow = { + workspaceId: 'ws-1', + userId: 'owner-user', + context: 'output', + deletedAt: null, + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('denies a workspace member who is not the owning chat user (explicit output context)', async () => { + mockGetFileMetadataByKey.mockResolvedValue(outputRow) + mockGetUserEntityPermissions.mockResolvedValue('read') + + await expect(verifyFileAccess(OUTPUT_KEY, 'other-member', undefined, 'output')).resolves.toBe( + false + ) + }) + + it('denies a non-owner member when context is inferred as workspace (serve path)', async () => { + mockGetFileMetadataByKey.mockResolvedValue(outputRow) + mockGetUserEntityPermissions.mockResolvedValue('read') + + await expect( + verifyFileAccess(OUTPUT_KEY, 'other-member', undefined, 'workspace') + ).resolves.toBe(false) + }) + + it('grants the owning chat user with workspace membership', async () => { + mockGetFileMetadataByKey.mockResolvedValue(outputRow) + mockGetUserEntityPermissions.mockResolvedValue('read') + + await expect(verifyFileAccess(OUTPUT_KEY, 'owner-user', undefined, 'output')).resolves.toBe( + true + ) + }) + + it('still denies the owner without workspace membership (left the workspace)', async () => { + mockGetFileMetadataByKey.mockResolvedValue(outputRow) + mockGetUserEntityPermissions.mockResolvedValue(null) + + await expect(verifyFileAccess(OUTPUT_KEY, 'owner-user', undefined, 'output')).resolves.toBe( + false + ) + }) + + it('leaves plain workspace files on membership-only auth', async () => { + mockGetFileMetadataByKey.mockResolvedValue({ ...outputRow, context: 'workspace' }) + mockGetUserEntityPermissions.mockResolvedValue('read') + + await expect( + verifyFileAccess(OUTPUT_KEY, 'other-member', undefined, 'workspace') + ).resolves.toBe(true) + }) +}) diff --git a/apps/sim/app/api/files/authorization.ts b/apps/sim/app/api/files/authorization.ts index 8719e28bdb4..240476b3742 100644 --- a/apps/sim/app/api/files/authorization.ts +++ b/apps/sim/app/api/files/authorization.ts @@ -60,6 +60,21 @@ function workspacePermissionSatisfies( return permissionSatisfies(permission, requireWrite ? 'write' : 'read') } +/** + * Chat-scoped `output` rows belong to a PRIVATE chat: workspace membership + * alone is not enough — the caller must also be the row's owner (stamped from + * the chat owner at creation and re-stamped on fork/duplicate). Without this, + * any workspace member who learns a file id or storage key could read another + * member's agent outputs, even though listing outputs requires chat ownership. + * Non-output contexts pass through unchanged. + */ +function outputOwnershipSatisfied( + record: { context: string; uploadedBy: string }, + userId: string +): boolean { + return record.context !== 'output' || record.uploadedBy === userId +} + /** * Lookup workspace file by storage key from database * @param key Storage key to lookup @@ -68,15 +83,17 @@ function workspacePermissionSatisfies( async function lookupWorkspaceFileByKey( key: string, options?: { includeDeleted?: boolean } -): Promise<{ workspaceId: string; uploadedBy: string } | null> { +): Promise<{ workspaceId: string; uploadedBy: string; context: string } | null> { try { const { includeDeleted = false } = options ?? {} // Priority 1: Check new workspaceFiles table. Look up by key across // WORKSPACE_FILE_LOOKUP_CONTEXTS (`workspace` + `output`): both share the - // `workspace//...` key shape and authorize by workspace membership. Filtering - // to `workspace` alone made `output` files unservable (broken previews); scoping to - // this explicit set (rather than dropping the filter) keeps outputs servable while - // leaving upload (`mothership`) authorization and the owner-scoped contexts untouched. + // `workspace//...` key shape. `workspace` rows authorize by workspace + // membership; `output` rows additionally require ownership (see + // outputOwnershipSatisfied). Filtering to `workspace` alone made `output` + // files unservable (broken previews); scoping to this explicit set (rather + // than dropping the filter) keeps outputs servable while leaving upload + // (`mothership`) authorization and the owner-scoped contexts untouched. const fileRecord = await getFileMetadataByKey(key, WORKSPACE_FILE_LOOKUP_CONTEXTS, { includeDeleted, }) @@ -85,6 +102,7 @@ async function lookupWorkspaceFileByKey( return { workspaceId: fileRecord.workspaceId || '', uploadedBy: fileRecord.userId, + context: fileRecord.context, } } @@ -107,6 +125,7 @@ async function lookupWorkspaceFileByKey( return { workspaceId: legacyFile.workspaceId, uploadedBy: legacyFile.uploadedBy, + context: 'workspace', } } } catch (legacyError) { @@ -181,8 +200,15 @@ export async function verifyFileAccess( return true } - // 1. Workspace / mothership files: Check database first (most reliable for both local and cloud) - if (inferredContext === 'workspace' || inferredContext === 'mothership') { + // 1. Workspace / mothership / chat-output files: check database first (most + // reliable for both local and cloud). `output` shares the workspace key + // shape; explicitly routing it here (instead of letting it fall through to + // verifyRegularFileAccess) keeps its ownership rule applied on every path. + if ( + inferredContext === 'workspace' || + inferredContext === 'mothership' || + inferredContext === 'output' + ) { return await verifyWorkspaceFileAccess(cloudKey, userId, customConfig, isLocal, requireWrite) } @@ -242,6 +268,14 @@ async function verifyWorkspaceFileAccess( // Priority 1: Check database (most reliable, works for both local and cloud) const workspaceFileRecord = await lookupWorkspaceFileByKey(cloudKey) if (workspaceFileRecord) { + if (!outputOwnershipSatisfied(workspaceFileRecord, userId)) { + logger.warn('Chat output file access denied: caller is not the owning chat user', { + userId, + workspaceId: workspaceFileRecord.workspaceId, + cloudKey, + }) + return false + } const permission = await getUserEntityPermissions( userId, 'workspace', @@ -671,6 +705,14 @@ async function verifyRegularFileAccess( // This handles legacy files that might not have metadata const workspaceFileRecord = await lookupWorkspaceFileByKey(cloudKey) if (workspaceFileRecord) { + if (!outputOwnershipSatisfied(workspaceFileRecord, userId)) { + logger.warn('Chat output file access denied: caller is not the owning chat user', { + userId, + workspaceId: workspaceFileRecord.workspaceId, + cloudKey, + }) + return false + } const permission = await getUserEntityPermissions( userId, 'workspace', diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts index 3e50da6a7fc..33ef0996ac8 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts @@ -51,7 +51,7 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) } - const file = await getPreviewableWorkspaceFile(workspaceId, fileId) + const file = await getPreviewableWorkspaceFile(workspaceId, fileId, session.user.id) if (!file) { return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 }) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index bdc522a5236..ca71fda37bc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -36,6 +36,7 @@ import { Workflow, } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' import { MoreHorizontal, Pin } from 'lucide-react' import Link from 'next/link' import { useParams, usePathname, useRouter } from 'next/navigation' @@ -982,6 +983,9 @@ export const Sidebar = memo(function Sidebar({ isCollapsed }: SidebarProps) { useFolderStore.getState().clearChatSelection() navigateToPage(`/workspace/${workspaceId}/chat/${result.id}`) }, + onError: (error) => { + toast.error(getErrorMessage(error, 'Failed to duplicate chat')) + }, } ) }, [navigateToPage, workspaceId]) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 9c0b444e53d..904170dbcca 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -989,8 +989,10 @@ export async function getWorkspaceFile( * Fetch a single file record by id for PREVIEW, including the chat-scoped `output` * context (agent-generated outputs) that never appears in the workspace Files list. * Returns the same shape as {@link listWorkspaceFiles} so the resource panel can - * render an output that {@link getWorkspaceFile}/list would miss. Authorization - * (workspace membership) is the caller's responsibility. + * render an output that {@link getWorkspaceFile}/list would miss. Workspace + * membership is the caller's responsibility; chat-output OWNERSHIP is enforced + * here — `output` rows belong to a private chat, so only the owning chat's user + * may resolve them (non-owners get null, indistinguishable from a missing id). * * `mothership` chat uploads are intentionally not included here — surfacing uploads * through this preview path is out of scope for the outputs feature (see @@ -998,7 +1000,8 @@ export async function getWorkspaceFile( */ export async function getPreviewableWorkspaceFile( workspaceId: string, - fileId: string + fileId: string, + requestingUserId: string ): Promise { try { const [file] = await db @@ -1015,6 +1018,14 @@ export async function getPreviewableWorkspaceFile( .limit(1) if (!file) return null + if (file.context === 'output' && file.userId !== requestingUserId) { + logger.warn('Chat output preview denied: caller is not the owning chat user', { + fileId, + workspaceId, + requestingUserId, + }) + return null + } return mapSingleWorkspaceFileRecord(file, workspaceId) } catch (error) { logger.error(`Failed to get previewable workspace file ${fileId}:`, error) From 4e82a2536ab2657b0dc75e1dea4255cf251562c7 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:00:51 -0700 Subject: [PATCH 10/10] chore(api): bump route-count baseline for the chat outputs route --- scripts/check-api-validation-contracts.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 4858463d4a5..5cfcfa50b3d 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,10 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 884, - zodRoutes: 884, + // 884 -> 885: GET /api/mothership/chats/[chatId]/outputs (contract-bound via + // listChatOutputsContract; lists a chat's agent-generated output files). + totalRoutes: 885, + zodRoutes: 885, nonZodRoutes: 0, } as const