From 704d06fcfd1d8a281e25998f8df6cfe8ed5b9cfb Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 12:21:15 -0700 Subject: [PATCH 01/31] v0 --- .../app/api/files/serve/[...path]/route.ts | 58 +++++- .../components/file-viewer/file-viewer.tsx | 187 +++++++++++++++++- .../components/file-viewer/preview-panel.tsx | 1 + .../resource-content/resource-content.tsx | 46 +++-- .../[workspaceId]/home/hooks/use-chat.ts | 10 +- .../w/[workflowId]/components/panel/panel.tsx | 36 ++-- apps/sim/hooks/queries/workspace-files.ts | 53 ++++- .../tools/server/files/workspace-file.ts | 141 ++++++++++++- apps/sim/lib/copilot/tools/shared/schemas.ts | 6 +- apps/sim/lib/copilot/vfs/file-reader.ts | 25 ++- .../contexts/copilot/copilot-file-manager.ts | 2 + .../workspace/workspace-file-manager.ts | 10 +- apps/sim/package.json | 5 +- bun.lock | 17 ++ 14 files changed, 524 insertions(+), 73 deletions(-) diff --git a/apps/sim/app/api/files/serve/[...path]/route.ts b/apps/sim/app/api/files/serve/[...path]/route.ts index 94d882a86e8..58a455c0d8c 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.ts @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generatePptxFromCode } from '@/lib/copilot/tools/server/files/workspace-file' import { CopilotFiles, isUsingCloudStorage } from '@/lib/uploads' import type { StorageContext } from '@/lib/uploads/config' import { downloadFile } from '@/lib/uploads/core/storage-service' @@ -18,6 +19,27 @@ import { const logger = createLogger('FilesServeAPI') +const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]) + +async function compilePptxIfNeeded( + buffer: Buffer, + filename: string, + workspaceId?: string, + raw?: boolean +): Promise<{ buffer: Buffer; contentType: string }> { + const isPptx = filename.toLowerCase().endsWith('.pptx') + if (raw || !isPptx || buffer.subarray(0, 4).equals(ZIP_MAGIC)) { + return { buffer, contentType: getContentType(filename) } + } + + const code = buffer.toString('utf-8') + const compiled = await generatePptxFromCode(code, workspaceId || '') + return { + buffer: compiled, + contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + } +} + const STORAGE_KEY_PREFIX_RE = /^\d{13}-[a-z0-9]{7}-/ function stripStorageKeyPrefix(segment: string): string { @@ -44,6 +66,7 @@ export async function GET( const cloudKey = isCloudPath ? path.slice(1).join('/') : fullPath const contextParam = request.nextUrl.searchParams.get('context') + const raw = request.nextUrl.searchParams.get('raw') === '1' const context = contextParam || (isCloudPath ? inferContextFromKey(cloudKey) : undefined) @@ -68,10 +91,10 @@ export async function GET( const userId = authResult.userId if (isUsingCloudStorage()) { - return await handleCloudProxy(cloudKey, userId, contextParam) + return await handleCloudProxy(cloudKey, userId, contextParam, raw) } - return await handleLocalFile(cloudKey, userId) + return await handleLocalFile(cloudKey, userId, raw) } catch (error) { logger.error('Error serving file:', error) @@ -83,7 +106,11 @@ export async function GET( } } -async function handleLocalFile(filename: string, userId: string): Promise { +async function handleLocalFile( + filename: string, + userId: string, + raw: boolean +): Promise { try { const contextParam: StorageContext | undefined = inferContextFromKey(filename) as | StorageContext @@ -108,10 +135,15 @@ async function handleLocalFile(filename: string, userId: string): Promise { try { let context: StorageContext @@ -156,12 +189,12 @@ async function handleCloudProxy( throw new FileNotFoundError(`File not found: ${cloudKey}`) } - let fileBuffer: Buffer + let rawBuffer: Buffer if (context === 'copilot') { - fileBuffer = await CopilotFiles.downloadCopilotFile(cloudKey) + rawBuffer = await CopilotFiles.downloadCopilotFile(cloudKey) } else { - fileBuffer = await downloadFile({ + rawBuffer = await downloadFile({ key: cloudKey, context, }) @@ -169,7 +202,12 @@ async function handleCloudProxy( const segment = cloudKey.split('/').pop() || 'download' const displayName = stripStorageKeyPrefix(segment) - const contentType = getContentType(displayName) + const { buffer: fileBuffer, contentType } = await compilePptxIfNeeded( + rawBuffer, + displayName, + undefined, + raw + ) logger.info('Cloud file served', { userId, diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index a0894b41a94..132ed82529e 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -8,6 +8,7 @@ import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import { getFileExtension } from '@/lib/uploads/utils/file-utils' import { useUpdateWorkspaceFileContent, + useWorkspaceFileBinary, useWorkspaceFileContent, } from '@/hooks/queries/workspace-files' import { useAutosave } from '@/hooks/use-autosave' @@ -48,17 +49,29 @@ const IFRAME_PREVIEWABLE_EXTENSIONS = new Set(['pdf']) const IMAGE_PREVIEWABLE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']) const IMAGE_PREVIEWABLE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp']) -type FileCategory = 'text-editable' | 'iframe-previewable' | 'image-previewable' | 'unsupported' +const PPTX_PREVIEWABLE_MIME_TYPES = new Set([ + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', +]) +const PPTX_PREVIEWABLE_EXTENSIONS = new Set(['pptx']) + +type FileCategory = + | 'text-editable' + | 'iframe-previewable' + | 'image-previewable' + | 'pptx-previewable' + | 'unsupported' function resolveFileCategory(mimeType: string | null, filename: string): FileCategory { if (mimeType && TEXT_EDITABLE_MIME_TYPES.has(mimeType)) return 'text-editable' if (mimeType && IFRAME_PREVIEWABLE_MIME_TYPES.has(mimeType)) return 'iframe-previewable' if (mimeType && IMAGE_PREVIEWABLE_MIME_TYPES.has(mimeType)) return 'image-previewable' + if (mimeType && PPTX_PREVIEWABLE_MIME_TYPES.has(mimeType)) return 'pptx-previewable' const ext = getFileExtension(filename) if (TEXT_EDITABLE_EXTENSIONS.has(ext)) return 'text-editable' if (IFRAME_PREVIEWABLE_EXTENSIONS.has(ext)) return 'iframe-previewable' if (IMAGE_PREVIEWABLE_EXTENSIONS.has(ext)) return 'image-previewable' + if (PPTX_PREVIEWABLE_EXTENSIONS.has(ext)) return 'pptx-previewable' return 'unsupported' } @@ -124,6 +137,10 @@ export function FileViewer({ return } + if (category === 'pptx-previewable') { + return + } + return } @@ -163,7 +180,12 @@ function TextEditor({ isLoading, error, dataUpdatedAt, - } = useWorkspaceFileContent(workspaceId, file.id, file.key) + } = useWorkspaceFileContent( + workspaceId, + file.id, + file.key, + file.type === 'text/x-pptxgenjs' + ) const updateContent = useUpdateWorkspaceFileContent() @@ -417,6 +439,167 @@ function ImagePreview({ file }: { file: WorkspaceFileRecord }) { ) } +function PptxPreview({ + file, + workspaceId, + streamingContent, +}: { + file: WorkspaceFileRecord + workspaceId: string + streamingContent?: string +}) { + const { + data: fileData, + isLoading: isFetching, + error: fetchError, + dataUpdatedAt, + } = useWorkspaceFileBinary(workspaceId, file.id, file.key) + + const [slides, setSlides] = useState([]) + const [rendering, setRendering] = useState(false) + const [renderError, setRenderError] = useState(null) + + useEffect(() => { + let cancelled = false + + async function render() { + try { + setRendering(true) + setRenderError(null) + + if (streamingContent !== undefined) { + const PptxGenJS = (await import('pptxgenjs')).default + const pptx = new PptxGenJS() + const fn = new Function('pptx', `return (async () => { ${streamingContent} })()`) + await fn(pptx) + const arrayBuffer = (await pptx.write({ outputType: 'arraybuffer' })) as ArrayBuffer + if (cancelled) return + const { PPTXViewer } = await import('pptxviewjs') + const data = new Uint8Array(arrayBuffer) + const probe = document.createElement('canvas') + const probeViewer = new PPTXViewer({ canvas: probe }) + await probeViewer.loadFile(data) + const count = probeViewer.getSlideCount() + if (cancelled || count === 0) return + const dpr = window.devicePixelRatio || 1 + const W = Math.round(1920 * dpr) + const H = Math.round(1080 * dpr) + const images: string[] = [] + for (let i = 0; i < count; i++) { + if (cancelled) break + const canvas = document.createElement('canvas') + canvas.width = W + canvas.height = H + const viewer = new PPTXViewer({ canvas }) + await viewer.loadFile(data) + if (i > 0) await viewer.goToSlide(i) + else await viewer.render() + images.push(canvas.toDataURL('image/png')) + } + if (!cancelled) setSlides(images) + return + } + + if (!fileData) return + const { PPTXViewer } = await import('pptxviewjs') + if (cancelled) return + + const data = new Uint8Array(fileData!) + const probe = document.createElement('canvas') + const probeViewer = new PPTXViewer({ canvas: probe }) + await probeViewer.loadFile(data) + const count = probeViewer.getSlideCount() + if (cancelled || count === 0) return + + const dpr = window.devicePixelRatio || 1 + const W = Math.round(1920 * dpr) + const H = Math.round(1080 * dpr) + const images: string[] = [] + + for (let i = 0; i < count; i++) { + if (cancelled) break + const canvas = document.createElement('canvas') + canvas.width = W + canvas.height = H + const viewer = new PPTXViewer({ canvas }) + await viewer.loadFile(data) + if (i > 0) await viewer.goToSlide(i) + else await viewer.render() + images.push(canvas.toDataURL('image/png')) + } + + if (!cancelled) setSlides(images) + } catch (err) { + if (!cancelled) { + const msg = err instanceof Error ? err.message : 'Failed to render presentation' + logger.error('PPTX render failed', { error: msg }) + setRenderError(msg) + } + } finally { + if (!cancelled) setRendering(false) + } + } + + render() + return () => { + cancelled = true + } + }, [fileData, dataUpdatedAt, streamingContent]) + + const error = fetchError + ? fetchError instanceof Error + ? fetchError.message + : 'Failed to load file' + : renderError + const loading = isFetching || rendering + + if (error) { + return ( +
+

+ Failed to preview presentation +

+

{error}

+
+ ) + } + + if (loading && slides.length === 0) { + return ( +
+
+
+

Loading presentation...

+
+
+ ) + } + + return ( +
+
+ {slides.map((src, i) => ( + {`Slide + ))} +
+
+ ) +} + function UnsupportedPreview({ file }: { file: WorkspaceFileRecord }) { const ext = getFileExtension(file.name) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx index 0a7be9495ef..6cb4c19c6d9 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx @@ -327,3 +327,4 @@ function parseCsvLine(line: string, delimiter: string): string[] { fields.push(current.trim()) return fields } + 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 472bc6ca0cb..93c8176c71a 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 @@ -10,7 +10,7 @@ import { markRunToolManuallyStopped, reportManualRunToolStop, } from '@/lib/copilot/client-sse/run-tool-execution' -import { downloadWorkspaceFile } from '@/lib/uploads/utils/file-utils' +import { downloadWorkspaceFile, getMimeTypeFromExtension, getFileExtension } from '@/lib/uploads/utils/file-utils' import { FileViewer, type PreviewMode, @@ -64,35 +64,43 @@ export const ResourceContent = memo(function ResourceContent({ streamingFile, }: ResourceContentProps) { const streamFileName = streamingFile?.fileName || 'file.md' - const streamingExtractedContent = useMemo( - () => (streamingFile ? extractFileContent(streamingFile.content) : ''), - [streamingFile] - ) - const syntheticFile = useMemo( - () => ({ + const streamingExtractedContent = useMemo(() => { + if (!streamingFile) return undefined + const extracted = extractFileContent(streamingFile.content) + return extracted.length > 0 ? extracted : undefined + }, [streamingFile]) + const syntheticFile = useMemo(() => { + const ext = getFileExtension(streamFileName) + const type = ext === 'pptx' ? 'text/x-pptxgenjs' : getMimeTypeFromExtension(ext) + return { id: 'streaming-file', workspaceId, name: streamFileName, key: '', path: '', size: 0, - type: 'text/plain', + type, uploadedBy: '', uploadedAt: STREAMING_EPOCH, - }), - [workspaceId, streamFileName] - ) + } + }, [workspaceId, streamFileName]) if (streamingFile && resource.id === 'streaming-file') { return (
- + {streamingExtractedContent !== undefined ? ( + + ) : ( +
+

Processing file...

+
+ )}
) } @@ -108,7 +116,7 @@ export const ResourceContent = memo(function ResourceContent({ workspaceId={workspaceId} fileId={resource.id} previewMode={previewMode} - streamingContent={streamingFile ? extractFileContent(streamingFile.content) : undefined} + streamingContent={streamingExtractedContent} /> ) 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 048634f080a..0bd9551c8f4 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -987,15 +987,19 @@ export function useChat( onResourceEventRef.current?.() if (resource.type === 'workflow') { + const registry = useWorkflowRegistry.getState() const wasRegistered = ensureWorkflowInRegistry( resource.id, resource.title, workspaceId ) if (wasAdded && wasRegistered) { - useWorkflowRegistry.getState().setActiveWorkflow(resource.id) - } else { - useWorkflowRegistry.getState().loadWorkflowState(resource.id) + registry.setActiveWorkflow(resource.id) + } else if ( + registry.activeWorkflowId !== resource.id || + registry.hydration.phase !== 'ready' + ) { + registry.loadWorkflowState(resource.id) } } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index baf45ada2ff..cdeb54a94a2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -75,6 +75,7 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { getWorkflowWithValues } from '@/stores/workflows' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import type { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('Panel') /** @@ -290,18 +291,29 @@ export const Panel = memo(function Panel() { [copilotChatId, loadCopilotChats] ) - const onToolResult = useCallback( - (toolName: string, success: boolean, _result: unknown) => { - if (toolName === 'edit_workflow' && success && activeWorkflowId) { - fetch(`/api/workflows/${activeWorkflowId}/state`) - .then((res) => (res.ok ? res.json() : null)) - .then((freshState) => { - if (freshState) { - useWorkflowDiffStore.getState().setProposedChanges(freshState) - } + const handleCopilotToolResult = useCallback( + (toolName: string, success: boolean, output: unknown) => { + if (toolName !== 'edit_workflow' || !success) return + const workflowId = activeWorkflowId || useWorkflowRegistry.getState().activeWorkflowId + if (!workflowId) return + + fetch(`/api/workflows/${workflowId}/state`) + .then((res) => { + if (!res.ok) throw new Error(`State fetch failed: ${res.status}`) + return res.json() + }) + .then((freshState) => { + const diffStore = useWorkflowDiffStore.getState() + return diffStore.setProposedChanges(freshState as WorkflowState, undefined, { + skipPersist: true, }) - .catch(() => {}) - } + }) + .catch((err) => { + logger.error('Failed to fetch/apply edit_workflow state', { + error: err instanceof Error ? err.message : String(err), + workflowId, + }) + }) }, [activeWorkflowId] ) @@ -320,8 +332,8 @@ export const Panel = memo(function Panel() { apiPath: '/api/copilot/chat', stopPath: '/api/mothership/chat/stop', workflowId: activeWorkflowId || undefined, - onToolResult, onTitleUpdate: loadCopilotChats, + onToolResult: handleCopilotToolResult, }) const handleCopilotNewChat = useCallback(() => { diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index 2ac4d7b9d3e..1fe5349d74b 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -16,8 +16,11 @@ export const workspaceFilesKeys = { list: (workspaceId: string, scope: WorkspaceFileQueryScope = 'active') => [...workspaceFilesKeys.lists(), workspaceId, scope] as const, contents: () => [...workspaceFilesKeys.all, 'content'] as const, - content: (workspaceId: string, fileId: string) => - [...workspaceFilesKeys.contents(), workspaceId, fileId] as const, + content: ( + workspaceId: string, + fileId: string, + mode: 'text' | 'raw' | 'binary' = 'text' + ) => [...workspaceFilesKeys.contents(), workspaceId, fileId, mode] as const, storageInfo: () => [...workspaceFilesKeys.all, 'storageInfo'] as const, } @@ -66,8 +69,12 @@ export function useWorkspaceFiles(workspaceId: string, scope: WorkspaceFileQuery /** * Fetch file content as text via the serve URL */ -async function fetchWorkspaceFileContent(key: string, signal?: AbortSignal): Promise { - const serveUrl = `/api/files/serve/${encodeURIComponent(key)}?context=workspace&t=${Date.now()}` +async function fetchWorkspaceFileContent( + key: string, + signal?: AbortSignal, + raw?: boolean +): Promise { + const serveUrl = `/api/files/serve/${encodeURIComponent(key)}?context=workspace&t=${Date.now()}${raw ? '&raw=1' : ''}` const response = await fetch(serveUrl, { signal, cache: 'no-store' }) if (!response.ok) { @@ -80,10 +87,40 @@ async function fetchWorkspaceFileContent(key: string, signal?: AbortSignal): Pro /** * Hook to fetch workspace file content as text */ -export function useWorkspaceFileContent(workspaceId: string, fileId: string, key: string) { +export function useWorkspaceFileContent( + workspaceId: string, + fileId: string, + key: string, + raw?: boolean +) { return useQuery({ - queryKey: workspaceFilesKeys.content(workspaceId, fileId), - queryFn: ({ signal }) => fetchWorkspaceFileContent(key, signal), + queryKey: workspaceFilesKeys.content(workspaceId, fileId, raw ? 'raw' : 'text'), + queryFn: ({ signal }) => fetchWorkspaceFileContent(key, signal, raw), + enabled: !!workspaceId && !!fileId && !!key, + staleTime: 30 * 1000, + refetchOnWindowFocus: 'always', + }) +} + +async function fetchWorkspaceFileBinary( + key: string, + signal?: AbortSignal +): Promise { + const serveUrl = `/api/files/serve/${encodeURIComponent(key)}?context=workspace&t=${Date.now()}` + const response = await fetch(serveUrl, { signal, cache: 'no-store' }) + if (!response.ok) throw new Error('Failed to fetch file content') + return response.arrayBuffer() +} + +/** + * Hook to fetch workspace file content as binary (ArrayBuffer). + * Shares the same query key as useWorkspaceFileContent so cache + * invalidation from file updates triggers a refetch automatically. + */ +export function useWorkspaceFileBinary(workspaceId: string, fileId: string, key: string) { + return useQuery({ + queryKey: workspaceFilesKeys.content(workspaceId, fileId, 'binary'), + queryFn: ({ signal }) => fetchWorkspaceFileBinary(key, signal), enabled: !!workspaceId && !!fileId && !!key, staleTime: 30 * 1000, refetchOnWindowFocus: 'always', @@ -202,7 +239,7 @@ export function useUpdateWorkspaceFileContent() { }, onSettled: (_data, _error, variables) => { queryClient.invalidateQueries({ - queryKey: workspaceFilesKeys.content(variables.workspaceId, variables.fileId), + queryKey: [...workspaceFilesKeys.contents(), variables.workspaceId, variables.fileId], }) queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() }) 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 75c6abe6110..ea73fab7f7c 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -1,8 +1,10 @@ +import PptxGenJS from 'pptxgenjs' import { createLogger } from '@sim/logger' import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' import type { WorkspaceFileArgs, WorkspaceFileResult } from '@/lib/copilot/tools/shared/schemas' import { deleteWorkspaceFile, + downloadWorkspaceFile as downloadWsFile, getWorkspaceFile, renameWorkspaceFile, updateWorkspaceFileContent, @@ -11,12 +13,33 @@ import { const logger = createLogger('WorkspaceFileServerTool') +const PPTX_MIME = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' +const PPTX_SOURCE_MIME = 'text/x-pptxgenjs' + const EXT_TO_MIME: Record = { '.txt': 'text/plain', '.md': 'text/markdown', '.html': 'text/html', '.json': 'application/json', '.csv': 'text/csv', + '.pptx': PPTX_MIME, +} + +export async function generatePptxFromCode(code: string, workspaceId: string): Promise { + const pptx = new PptxGenJS() + + async function getFileBase64(fileId: string): Promise { + const record = await getWorkspaceFile(workspaceId, fileId) + if (!record) throw new Error(`File not found: ${fileId}`) + const buffer = await downloadWsFile(record) + const mime = record.type || 'image/png' + return `data:${mime};base64,${buffer.toString('base64')}` + } + + const fn = new Function('pptx', 'getFileBase64', `return (async () => { ${code} })()`) + await fn(pptx, getFileBase64) + const output = await pptx.write({ outputType: 'nodebuffer' }) + return output as Buffer } function inferContentType(fileName: string, explicitType?: string): string { @@ -58,8 +81,28 @@ export const workspaceFileServerTool: BaseServerTool).fileId as string | undefined + const edits = (args as Record).edits as + | { search: string; replace: string }[] + | undefined + + if (!fileId) { + return { success: false, message: 'fileId is required for patch operation' } + } + if (!edits || !Array.isArray(edits) || edits.length === 0) { + return { success: false, message: 'edits array is required for patch operation' } + } + + const fileRecord = await getWorkspaceFile(workspaceId, fileId) + if (!fileRecord) { + return { success: false, message: `File with ID "${fileId}" not found` } + } + + const currentBuffer = await downloadWsFile(fileRecord) + let content = currentBuffer.toString('utf-8') + + for (const edit of edits) { + const idx = content.indexOf(edit.search) + if (idx === -1) { + return { + success: false, + message: `Patch failed: search string not found in file "${fileRecord.name}". Search: "${edit.search.slice(0, 100)}${edit.search.length > 100 ? '...' : ''}"`, + } + } + content = content.slice(0, idx) + edit.replace + content.slice(idx + edit.search.length) + } + + const isPptxPatch = fileRecord.name?.toLowerCase().endsWith('.pptx') + if (isPptxPatch) { + try { + await generatePptxFromCode(content, workspaceId) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + return { + success: false, + message: `Patched PPTX code failed to compile: ${msg}. Fix the edits and retry.`, + } + } + } + + const patchedBuffer = Buffer.from(content, 'utf-8') + await updateWorkspaceFileContent( + workspaceId, + fileId, + context.userId, + patchedBuffer, + isPptxPatch ? PPTX_SOURCE_MIME : undefined + ) + + logger.info('Workspace file patched via copilot', { + fileId, + name: fileRecord.name, + editCount: edits.length, + userId: context.userId, + }) + + return { + success: true, + message: `File "${fileRecord.name}" patched successfully (${edits.length} edit${edits.length > 1 ? 's' : ''} applied)`, + data: { + id: fileId, + name: fileRecord.name, + size: patchedBuffer.length, + }, + } + } + default: return { success: false, - message: `Unknown operation: ${operation}. Supported: write, update, rename, delete. Use the filesystem to list/read files.`, + message: `Unknown operation: ${operation}. Supported: write, update, patch, rename, delete.`, } } } catch (error) { diff --git a/apps/sim/lib/copilot/tools/shared/schemas.ts b/apps/sim/lib/copilot/tools/shared/schemas.ts index 26a2f391134..8fd717f1aac 100644 --- a/apps/sim/lib/copilot/tools/shared/schemas.ts +++ b/apps/sim/lib/copilot/tools/shared/schemas.ts @@ -173,7 +173,7 @@ export type UserTableResult = z.infer // workspace_file - shared schema used by server tool and Go catalog export const WorkspaceFileArgsSchema = z.object({ - operation: z.enum(['write', 'update', 'delete', 'rename']), + operation: z.enum(['write', 'update', 'delete', 'rename', 'patch']), args: z .object({ fileId: z.string().optional(), @@ -181,8 +181,10 @@ export const WorkspaceFileArgsSchema = z.object({ content: z.string().optional(), contentType: z.string().optional(), workspaceId: z.string().optional(), - /** New name for the file (required for rename operation) */ newName: z.string().optional(), + edits: z + .array(z.object({ search: z.string(), replace: z.string() })) + .optional(), }) .optional(), }) diff --git a/apps/sim/lib/copilot/vfs/file-reader.ts b/apps/sim/lib/copilot/vfs/file-reader.ts index b22e2395c7a..87e37fdc72d 100644 --- a/apps/sim/lib/copilot/vfs/file-reader.ts +++ b/apps/sim/lib/copilot/vfs/file-reader.ts @@ -14,6 +14,7 @@ const TEXT_TYPES = new Set([ 'text/markdown', 'text/html', 'text/xml', + 'text/x-pptxgenjs', 'application/json', 'application/xml', 'application/javascript', @@ -72,6 +73,19 @@ export async function readFileRecord(record: WorkspaceFileRecord): Promise MAX_TEXT_READ_BYTES) { + return { + content: `[File too large to display inline: ${record.name} (${record.size} bytes, limit ${MAX_TEXT_READ_BYTES})]`, + totalLines: 1, + } + } + + const buffer = await downloadWorkspaceFile(record) + const content = buffer.toString('utf-8') + return { content, totalLines: content.split('\n').length } + } + const ext = getExtension(record.name) if (PARSEABLE_EXTENSIONS.has(ext)) { const buffer = await downloadWorkspaceFile(record) @@ -100,16 +114,7 @@ export async function readFileRecord(record: WorkspaceFileRecord): Promise MAX_TEXT_READ_BYTES) { - return { - content: `[File too large to display inline: ${record.name} (${record.size} bytes, limit ${MAX_TEXT_READ_BYTES})]`, - totalLines: 1, - } - } - - const buffer = await downloadWorkspaceFile(record) - const content = buffer.toString('utf-8') - return { content, totalLines: content.split('\n').length } + return null } catch (err) { logger.warn('Failed to read workspace file', { fileName: record.name, diff --git a/apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.ts b/apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.ts index da809071c50..da61df38fce 100644 --- a/apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.ts @@ -27,6 +27,8 @@ const SUPPORTED_FILE_TYPES = [ 'application/json', 'application/xml', 'text/xml', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'text/x-pptxgenjs', ] /** 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 29460aad032..c3819cd1b34 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -520,7 +520,8 @@ export async function updateWorkspaceFileContent( workspaceId: string, fileId: string, userId: string, - content: Buffer + content: Buffer, + contentType?: string ): Promise { logger.info(`Updating workspace file content: ${fileId} for workspace ${workspaceId}`) @@ -537,6 +538,8 @@ export async function updateWorkspaceFileContent( } } + const nextContentType = contentType || fileRecord.type + try { const metadata: Record = { originalName: fileRecord.name, @@ -549,7 +552,7 @@ export async function updateWorkspaceFileContent( await uploadFile({ file: content, fileName: fileRecord.key, - contentType: fileRecord.type, + contentType: nextContentType, context: 'workspace', preserveKey: true, customKey: fileRecord.key, @@ -558,7 +561,7 @@ export async function updateWorkspaceFileContent( await db .update(workspaceFiles) - .set({ size: content.length }) + .set({ size: content.length, contentType: nextContentType }) .where( and( eq(workspaceFiles.id, fileId), @@ -584,6 +587,7 @@ export async function updateWorkspaceFileContent( return { ...fileRecord, size: content.length, + type: nextContentType, } } catch (error) { logger.error(`Failed to update workspace file content ${fileId}:`, error) diff --git a/apps/sim/package.json b/apps/sim/package.json index 5c3fb544b95..6dab8edcf84 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -41,13 +41,13 @@ "@azure/storage-blob": "12.27.0", "@better-auth/sso": "1.3.12", "@better-auth/stripe": "1.3.12", - "@marsidev/react-turnstile": "1.4.2", "@browserbasehq/stagehand": "^3.0.5", "@cerebras/cerebras_cloud_sdk": "^1.23.0", "@e2b/code-interpreter": "^2.0.0", "@google/genai": "1.34.0", "@hookform/resolvers": "^4.1.3", "@linear/sdk": "40.0.0", + "@marsidev/react-turnstile": "1.4.2", "@modelcontextprotocol/sdk": "1.20.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-jaeger": "2.1.0", @@ -92,6 +92,7 @@ "binary-extensions": "^2.0.0", "browser-image-compression": "^2.0.2", "chalk": "5.6.2", + "chart.js": "4.5.1", "cheerio": "1.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -146,6 +147,8 @@ "postgres": "^3.4.5", "posthog-js": "1.334.1", "posthog-node": "5.9.2", + "pptxgenjs": "4.0.1", + "pptxviewjs": "1.1.8", "prismjs": "^1.30.0", "react": "19.2.4", "react-dom": "19.2.4", diff --git a/bun.lock b/bun.lock index 9af6d37ea56..dd644444895 100644 --- a/bun.lock +++ b/bun.lock @@ -117,6 +117,7 @@ "binary-extensions": "^2.0.0", "browser-image-compression": "^2.0.2", "chalk": "5.6.2", + "chart.js": "4.5.1", "cheerio": "1.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -171,6 +172,8 @@ "postgres": "^3.4.5", "posthog-js": "1.334.1", "posthog-node": "5.9.2", + "pptxgenjs": "4.0.1", + "pptxviewjs": "1.1.8", "prismjs": "^1.30.0", "react": "19.2.4", "react-dom": "19.2.4", @@ -802,6 +805,8 @@ "@jsonhero/path": ["@jsonhero/path@1.0.21", "", {}, "sha512-gVUDj/92acpVoJwsVJ/RuWOaHyG4oFzn898WNGQItLCTQ+hOaVlEaImhwE1WqOTf+l3dGOUkbSiVKlb3q1hd1Q=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@langchain/core": ["@langchain/core@0.3.80", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": "^0.3.67", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", "uuid": "^10.0.0", "zod": "^3.25.32", "zod-to-json-schema": "^3.22.3" } }, "sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA=="], "@langchain/openai": ["@langchain/openai@0.4.9", "", { "dependencies": { "js-tiktoken": "^1.0.12", "openai": "^4.87.3", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.3" }, "peerDependencies": { "@langchain/core": ">=0.3.39 <0.4.0" } }, "sha512-NAsaionRHNdqaMjVLPkFCyjUDze+OqRHghA1Cn4fPoAafz+FXcl9c7LlEl9Xo0FH6/8yiCl7Rw2t780C/SBVxQ=="], @@ -1858,6 +1863,8 @@ "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], "cheerio": ["cheerio@1.1.2", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.0.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg=="], @@ -2424,6 +2431,8 @@ "http-response-object": ["http-response-object@3.0.2", "", { "dependencies": { "@types/node": "^10.0.3" } }, "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA=="], + "https": ["https@1.0.0", "", {}, "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], @@ -3096,6 +3105,10 @@ "posthog-node": ["posthog-node@5.9.2", "", { "dependencies": { "@posthog/core": "1.2.2" } }, "sha512-oU7FbFcH5cn40nhP04cBeT67zE76EiGWjKKzDvm6IOm5P83sqM0Ij0wMJQSHp+QI6ZN7MLzb+4xfMPUEZ4q6CA=="], + "pptxgenjs": ["pptxgenjs@4.0.1", "", { "dependencies": { "@types/node": "^22.8.1", "https": "^1.0.0", "image-size": "^1.2.1", "jszip": "^3.10.1" } }, "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A=="], + + "pptxviewjs": ["pptxviewjs@1.1.8", "", { "peerDependencies": { "chart.js": ">=4.4.1", "jszip": ">=3.10.1" }, "optionalPeers": ["chart.js", "jszip"] }, "sha512-Nk3uIg1H7WkigKIKZPcTrcmV4RMpRSHvG4jWAO9aKPD1MWkOF8fwqtypsF+kzUZvIzO0BA/eKK+zNK7/R7WrDg=="], + "preact": ["preact@10.28.3", "", {}, "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA=="], "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], @@ -3142,6 +3155,8 @@ "query-selector-shadow-dom": ["query-selector-shadow-dom@1.0.1", "", {}, "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw=="], + "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], @@ -4280,6 +4295,8 @@ "posthog-node/@posthog/core": ["@posthog/core@1.2.2", "", {}, "sha512-f16Ozx6LIigRG+HsJdt+7kgSxZTHeX5f1JlCGKI1lXcvlZgfsCR338FuMI2QRYXGl+jg/vYFzGOTQBxl90lnBg=="], + "pptxgenjs/image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], + "protobufjs/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], From 8abb8846e447d816e61257072179f78ce5bece92 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 12:22:56 -0700 Subject: [PATCH 02/31] Fix ppt load --- .../components/file-viewer/file-viewer.tsx | 111 ++++++++++-------- apps/sim/hooks/queries/workspace-files.ts | 8 +- 2 files changed, 66 insertions(+), 53 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index 132ed82529e..af2a9e0fb8c 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -439,6 +439,39 @@ function ImagePreview({ file }: { file: WorkspaceFileRecord }) { ) } +const pptxSlideCache = new Map() + +function pptxCacheKey(fileId: string, dataUpdatedAt: number): string { + return `${fileId}:${dataUpdatedAt}` +} + +async function renderPptxSlides( + data: Uint8Array, + onSlide: (src: string, index: number) => void, + cancelled: () => boolean +): Promise { + const { PPTXViewer } = await import('pptxviewjs') + if (cancelled()) return + + const W = 1920 + const H = 1080 + + const canvas = document.createElement('canvas') + canvas.width = W + canvas.height = H + const viewer = new PPTXViewer({ canvas }) + await viewer.loadFile(data) + const count = viewer.getSlideCount() + if (cancelled() || count === 0) return + + for (let i = 0; i < count; i++) { + if (cancelled()) break + if (i === 0) await viewer.render() + else await viewer.goToSlide(i) + onSlide(canvas.toDataURL('image/jpeg', 0.85), i) + } +} + function PptxPreview({ file, workspaceId, @@ -455,11 +488,19 @@ function PptxPreview({ dataUpdatedAt, } = useWorkspaceFileBinary(workspaceId, file.id, file.key) - const [slides, setSlides] = useState([]) + const cacheKey = pptxCacheKey(file.id, dataUpdatedAt) + const cached = pptxSlideCache.get(cacheKey) + + const [slides, setSlides] = useState(cached ?? []) const [rendering, setRendering] = useState(false) const [renderError, setRenderError] = useState(null) useEffect(() => { + if (cached) { + setSlides(cached) + return + } + let cancelled = false async function render() { @@ -474,61 +515,33 @@ function PptxPreview({ await fn(pptx) const arrayBuffer = (await pptx.write({ outputType: 'arraybuffer' })) as ArrayBuffer if (cancelled) return - const { PPTXViewer } = await import('pptxviewjs') const data = new Uint8Array(arrayBuffer) - const probe = document.createElement('canvas') - const probeViewer = new PPTXViewer({ canvas: probe }) - await probeViewer.loadFile(data) - const count = probeViewer.getSlideCount() - if (cancelled || count === 0) return - const dpr = window.devicePixelRatio || 1 - const W = Math.round(1920 * dpr) - const H = Math.round(1080 * dpr) const images: string[] = [] - for (let i = 0; i < count; i++) { - if (cancelled) break - const canvas = document.createElement('canvas') - canvas.width = W - canvas.height = H - const viewer = new PPTXViewer({ canvas }) - await viewer.loadFile(data) - if (i > 0) await viewer.goToSlide(i) - else await viewer.render() - images.push(canvas.toDataURL('image/png')) - } - if (!cancelled) setSlides(images) + await renderPptxSlides( + data, + (src) => { + images.push(src) + if (!cancelled) setSlides([...images]) + }, + () => cancelled + ) return } if (!fileData) return - const { PPTXViewer } = await import('pptxviewjs') - if (cancelled) return - - const data = new Uint8Array(fileData!) - const probe = document.createElement('canvas') - const probeViewer = new PPTXViewer({ canvas: probe }) - await probeViewer.loadFile(data) - const count = probeViewer.getSlideCount() - if (cancelled || count === 0) return - - const dpr = window.devicePixelRatio || 1 - const W = Math.round(1920 * dpr) - const H = Math.round(1080 * dpr) + const data = new Uint8Array(fileData) const images: string[] = [] - - for (let i = 0; i < count; i++) { - if (cancelled) break - const canvas = document.createElement('canvas') - canvas.width = W - canvas.height = H - const viewer = new PPTXViewer({ canvas }) - await viewer.loadFile(data) - if (i > 0) await viewer.goToSlide(i) - else await viewer.render() - images.push(canvas.toDataURL('image/png')) + await renderPptxSlides( + data, + (src) => { + images.push(src) + if (!cancelled) setSlides([...images]) + }, + () => cancelled + ) + if (!cancelled && images.length > 0) { + pptxSlideCache.set(cacheKey, images) } - - if (!cancelled) setSlides(images) } catch (err) { if (!cancelled) { const msg = err instanceof Error ? err.message : 'Failed to render presentation' @@ -544,7 +557,7 @@ function PptxPreview({ return () => { cancelled = true } - }, [fileData, dataUpdatedAt, streamingContent]) + }, [fileData, dataUpdatedAt, streamingContent, cacheKey, cached]) const error = fetchError ? fetchError instanceof Error diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index 1fe5349d74b..a759c1bf4b0 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -106,8 +106,8 @@ async function fetchWorkspaceFileBinary( key: string, signal?: AbortSignal ): Promise { - const serveUrl = `/api/files/serve/${encodeURIComponent(key)}?context=workspace&t=${Date.now()}` - const response = await fetch(serveUrl, { signal, cache: 'no-store' }) + const serveUrl = `/api/files/serve/${encodeURIComponent(key)}?context=workspace` + const response = await fetch(serveUrl, { signal }) if (!response.ok) throw new Error('Failed to fetch file content') return response.arrayBuffer() } @@ -122,8 +122,8 @@ export function useWorkspaceFileBinary(workspaceId: string, fileId: string, key: queryKey: workspaceFilesKeys.content(workspaceId, fileId, 'binary'), queryFn: ({ signal }) => fetchWorkspaceFileBinary(key, signal), enabled: !!workspaceId && !!fileId && !!key, - staleTime: 30 * 1000, - refetchOnWindowFocus: 'always', + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, }) } From b28556fa14dafdcce81710e31ea0986e7e8e3723 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 12:32:04 -0700 Subject: [PATCH 03/31] Fixes --- .../components/file-viewer/file-viewer.tsx | 19 +++++++++++++++---- .../resource-registry/resource-registry.tsx | 2 +- apps/sim/hooks/queries/workspace-files.ts | 11 ++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index af2a9e0fb8c..9fbad53491f 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -439,12 +439,22 @@ function ImagePreview({ file }: { file: WorkspaceFileRecord }) { ) } +const PPTX_CACHE_MAX = 5 + const pptxSlideCache = new Map() function pptxCacheKey(fileId: string, dataUpdatedAt: number): string { return `${fileId}:${dataUpdatedAt}` } +function pptxCacheSet(key: string, slides: string[]): void { + pptxSlideCache.set(key, slides) + if (pptxSlideCache.size > PPTX_CACHE_MAX) { + const oldest = pptxSlideCache.keys().next().value + if (oldest !== undefined) pptxSlideCache.delete(oldest) + } +} + async function renderPptxSlides( data: Uint8Array, onSlide: (src: string, index: number) => void, @@ -453,8 +463,9 @@ async function renderPptxSlides( const { PPTXViewer } = await import('pptxviewjs') if (cancelled()) return - const W = 1920 - const H = 1080 + const dpr = Math.min(window.devicePixelRatio || 1, 2) + const W = Math.round(1920 * dpr) + const H = Math.round(1080 * dpr) const canvas = document.createElement('canvas') canvas.width = W @@ -540,7 +551,7 @@ function PptxPreview({ () => cancelled ) if (!cancelled && images.length > 0) { - pptxSlideCache.set(cacheKey, images) + pptxCacheSet(cacheKey, images) } } catch (err) { if (!cancelled) { @@ -557,7 +568,7 @@ function PptxPreview({ return () => { cancelled = true } - }, [fileData, dataUpdatedAt, streamingContent, cacheKey, cached]) + }, [fileData, dataUpdatedAt, streamingContent, cacheKey]) const error = fetchError ? fetchError instanceof Error 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 1586f397ff6..bf3d2db19f6 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 @@ -129,7 +129,7 @@ const RESOURCE_INVALIDATORS: Record< }, file: (qc, wId, id) => { qc.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) - qc.invalidateQueries({ queryKey: workspaceFilesKeys.content(wId, id) }) + qc.invalidateQueries({ queryKey: workspaceFilesKeys.contentFile(wId, id) }) qc.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() }) }, workflow: (qc, _wId) => { diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index a759c1bf4b0..b679865dad3 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -16,11 +16,13 @@ export const workspaceFilesKeys = { list: (workspaceId: string, scope: WorkspaceFileQueryScope = 'active') => [...workspaceFilesKeys.lists(), workspaceId, scope] as const, contents: () => [...workspaceFilesKeys.all, 'content'] as const, + contentFile: (workspaceId: string, fileId: string) => + [...workspaceFilesKeys.contents(), workspaceId, fileId] as const, content: ( workspaceId: string, fileId: string, mode: 'text' | 'raw' | 'binary' = 'text' - ) => [...workspaceFilesKeys.contents(), workspaceId, fileId, mode] as const, + ) => [...workspaceFilesKeys.contentFile(workspaceId, fileId), mode] as const, storageInfo: () => [...workspaceFilesKeys.all, 'storageInfo'] as const, } @@ -122,8 +124,7 @@ export function useWorkspaceFileBinary(workspaceId: string, fileId: string, key: queryKey: workspaceFilesKeys.content(workspaceId, fileId, 'binary'), queryFn: ({ signal }) => fetchWorkspaceFileBinary(key, signal), enabled: !!workspaceId && !!fileId && !!key, - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, + staleTime: 30 * 1000, }) } @@ -239,7 +240,7 @@ export function useUpdateWorkspaceFileContent() { }, onSettled: (_data, _error, variables) => { queryClient.invalidateQueries({ - queryKey: [...workspaceFilesKeys.contents(), variables.workspaceId, variables.fileId], + queryKey: workspaceFilesKeys.contentFile(variables.workspaceId, variables.fileId), }) queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() }) @@ -345,7 +346,7 @@ export function useDeleteWorkspaceFile() { onSettled: (_data, _error, variables) => { queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) queryClient.removeQueries({ - queryKey: workspaceFilesKeys.content(variables.workspaceId, variables.fileId), + queryKey: workspaceFilesKeys.contentFile(variables.workspaceId, variables.fileId), }) queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() }) }, From dde64aa46e26aa1880103259276b1455e740cef1 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 12:52:50 -0700 Subject: [PATCH 04/31] Fixes --- .../files/components/file-viewer/file-viewer.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index 9fbad53491f..8216491fdb4 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -439,8 +439,6 @@ function ImagePreview({ file }: { file: WorkspaceFileRecord }) { ) } -const PPTX_CACHE_MAX = 5 - const pptxSlideCache = new Map() function pptxCacheKey(fileId: string, dataUpdatedAt: number): string { @@ -449,7 +447,7 @@ function pptxCacheKey(fileId: string, dataUpdatedAt: number): string { function pptxCacheSet(key: string, slides: string[]): void { pptxSlideCache.set(key, slides) - if (pptxSlideCache.size > PPTX_CACHE_MAX) { + if (pptxSlideCache.size > 5) { const oldest = pptxSlideCache.keys().next().value if (oldest !== undefined) pptxSlideCache.delete(oldest) } From 4a537ff92fcf111d2de7003875a208f936fc7d45 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 12:53:03 -0700 Subject: [PATCH 05/31] Fix lint --- apps/sim/app/api/files/serve/[...path]/route.ts | 2 +- .../files/components/file-viewer/file-viewer.tsx | 14 ++------------ .../files/components/file-viewer/preview-panel.tsx | 1 - .../resource-content/resource-content.tsx | 6 +++++- apps/sim/hooks/queries/workspace-files.ts | 12 +++--------- .../copilot/tools/server/files/workspace-file.ts | 2 +- apps/sim/lib/copilot/tools/shared/schemas.ts | 4 +--- 7 files changed, 13 insertions(+), 28 deletions(-) diff --git a/apps/sim/app/api/files/serve/[...path]/route.ts b/apps/sim/app/api/files/serve/[...path]/route.ts index 58a455c0d8c..e8d32088173 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.ts @@ -163,7 +163,7 @@ async function handleCloudProxy( cloudKey: string, userId: string, contextParam?: string | null, - raw: boolean = false + raw = false ): Promise { try { let context: StorageContext diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index 8216491fdb4..04dd5d9725e 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -180,12 +180,7 @@ function TextEditor({ isLoading, error, dataUpdatedAt, - } = useWorkspaceFileContent( - workspaceId, - file.id, - file.key, - file.type === 'text/x-pptxgenjs' - ) + } = useWorkspaceFileContent(workspaceId, file.id, file.key, file.type === 'text/x-pptxgenjs') const updateContent = useUpdateWorkspaceFileContent() @@ -610,12 +605,7 @@ function PptxPreview({
{slides.map((src, i) => ( - {`Slide + {`Slide ))}
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx index 6cb4c19c6d9..0a7be9495ef 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx @@ -327,4 +327,3 @@ function parseCsvLine(line: string, delimiter: string): string[] { fields.push(current.trim()) return fields } - 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 93c8176c71a..9801e89b356 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 @@ -10,7 +10,11 @@ import { markRunToolManuallyStopped, reportManualRunToolStop, } from '@/lib/copilot/client-sse/run-tool-execution' -import { downloadWorkspaceFile, getMimeTypeFromExtension, getFileExtension } from '@/lib/uploads/utils/file-utils' +import { + downloadWorkspaceFile, + getFileExtension, + getMimeTypeFromExtension, +} from '@/lib/uploads/utils/file-utils' import { FileViewer, type PreviewMode, diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index b679865dad3..c4ad910a5c7 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -18,11 +18,8 @@ export const workspaceFilesKeys = { contents: () => [...workspaceFilesKeys.all, 'content'] as const, contentFile: (workspaceId: string, fileId: string) => [...workspaceFilesKeys.contents(), workspaceId, fileId] as const, - content: ( - workspaceId: string, - fileId: string, - mode: 'text' | 'raw' | 'binary' = 'text' - ) => [...workspaceFilesKeys.contentFile(workspaceId, fileId), mode] as const, + content: (workspaceId: string, fileId: string, mode: 'text' | 'raw' | 'binary' = 'text') => + [...workspaceFilesKeys.contentFile(workspaceId, fileId), mode] as const, storageInfo: () => [...workspaceFilesKeys.all, 'storageInfo'] as const, } @@ -104,10 +101,7 @@ export function useWorkspaceFileContent( }) } -async function fetchWorkspaceFileBinary( - key: string, - signal?: AbortSignal -): Promise { +async function fetchWorkspaceFileBinary(key: string, signal?: AbortSignal): Promise { const serveUrl = `/api/files/serve/${encodeURIComponent(key)}?context=workspace` const response = await fetch(serveUrl, { signal }) if (!response.ok) throw new Error('Failed to fetch file content') 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 ea73fab7f7c..b2596cea084 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -1,5 +1,5 @@ -import PptxGenJS from 'pptxgenjs' import { createLogger } from '@sim/logger' +import PptxGenJS from 'pptxgenjs' import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' import type { WorkspaceFileArgs, WorkspaceFileResult } from '@/lib/copilot/tools/shared/schemas' import { diff --git a/apps/sim/lib/copilot/tools/shared/schemas.ts b/apps/sim/lib/copilot/tools/shared/schemas.ts index 8fd717f1aac..3a0bd704a37 100644 --- a/apps/sim/lib/copilot/tools/shared/schemas.ts +++ b/apps/sim/lib/copilot/tools/shared/schemas.ts @@ -182,9 +182,7 @@ export const WorkspaceFileArgsSchema = z.object({ contentType: z.string().optional(), workspaceId: z.string().optional(), newName: z.string().optional(), - edits: z - .array(z.object({ search: z.string(), replace: z.string() })) - .optional(), + edits: z.array(z.object({ search: z.string(), replace: z.string() })).optional(), }) .optional(), }) From aa9fc102dd8d927fe08ffb447c1cdbde184a4831 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 13:41:12 -0700 Subject: [PATCH 06/31] Fix wid --- apps/sim/app/api/files/serve/[...path]/route.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/files/serve/[...path]/route.ts b/apps/sim/app/api/files/serve/[...path]/route.ts index e8d32088173..c2579156ba4 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.ts @@ -6,6 +6,7 @@ import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generatePptxFromCode } from '@/lib/copilot/tools/server/files/workspace-file' import { CopilotFiles, isUsingCloudStorage } from '@/lib/uploads' import type { StorageContext } from '@/lib/uploads/config' +import { parseWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { downloadFile } from '@/lib/uploads/core/storage-service' import { inferContextFromKey } from '@/lib/uploads/utils/file-utils' import { verifyFileAccess } from '@/app/api/files/authorization' @@ -46,6 +47,10 @@ function stripStorageKeyPrefix(segment: string): string { return STORAGE_KEY_PREFIX_RE.test(segment) ? segment.replace(STORAGE_KEY_PREFIX_RE, '') : segment } +function getWorkspaceIdForCompile(key: string): string | undefined { + return parseWorkspaceFileKey(key) ?? undefined +} + export async function GET( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } @@ -138,10 +143,11 @@ async function handleLocalFile( const rawBuffer = await readFile(filePath) const segment = filename.split('/').pop() || filename const displayName = stripStorageKeyPrefix(segment) + const workspaceId = getWorkspaceIdForCompile(filename) const { buffer: fileBuffer, contentType } = await compilePptxIfNeeded( rawBuffer, displayName, - undefined, + workspaceId, raw ) @@ -202,10 +208,11 @@ async function handleCloudProxy( const segment = cloudKey.split('/').pop() || 'download' const displayName = stripStorageKeyPrefix(segment) + const workspaceId = getWorkspaceIdForCompile(cloudKey) const { buffer: fileBuffer, contentType } = await compilePptxIfNeeded( rawBuffer, displayName, - undefined, + workspaceId, raw ) From 5954abd5ddd5d10c49c0a6ec4ae8debc7af1d488 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 15:21:32 -0700 Subject: [PATCH 07/31] Download image --- .../orchestrator/tool-executor/index.ts | 1 + apps/sim/lib/copilot/resource-extraction.ts | 2 + .../files/download-to-workspace-file.ts | 200 ++++++++++++++++++ apps/sim/lib/copilot/tools/server/router.ts | 2 + 4 files changed, 205 insertions(+) create mode 100644 apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts index 83d01040392..d5dfcb0754b 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts @@ -739,6 +739,7 @@ const SERVER_TOOLS = new Set([ 'knowledge_base', 'user_table', 'workspace_file', + 'download_to_workspace_file', 'get_execution_summary', 'get_job_logs', 'generate_visualization', diff --git a/apps/sim/lib/copilot/resource-extraction.ts b/apps/sim/lib/copilot/resource-extraction.ts index a1e9f75e3d8..94a7e9e160b 100644 --- a/apps/sim/lib/copilot/resource-extraction.ts +++ b/apps/sim/lib/copilot/resource-extraction.ts @@ -6,6 +6,7 @@ type ResourceType = MothershipResourceType const RESOURCE_TOOL_NAMES = new Set([ 'user_table', 'workspace_file', + 'download_to_workspace_file', 'create_workflow', 'edit_workflow', 'function_execute', @@ -119,6 +120,7 @@ export function extractResourcesFromToolResult( return [] } + case 'download_to_workspace_file': case 'generate_visualization': case 'generate_image': { if (result.fileId) { diff --git a/apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts new file mode 100644 index 00000000000..fe128473784 --- /dev/null +++ b/apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts @@ -0,0 +1,200 @@ +import { createLogger } from '@sim/logger' +import { z } from 'zod' +import { + assertServerToolNotAborted, + type BaseServerTool, + type ServerToolContext, +} from '@/lib/copilot/tools/server/base-tool' +import { + getExtensionFromMimeType, + getFileExtension, + getMimeTypeFromExtension, +} from '@/lib/uploads/utils/file-utils' +import { uploadWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager' + +const logger = createLogger('DownloadToWorkspaceFileTool') + +const DownloadToWorkspaceFileArgsSchema = z.object({ + url: z.string().url(), + fileName: z.string().min(1).optional(), +}) + +const DownloadToWorkspaceFileResultSchema = z.object({ + success: z.boolean(), + message: z.string(), + fileId: z.string().optional(), + fileName: z.string().optional(), + downloadUrl: z.string().optional(), +}) + +type DownloadToWorkspaceFileArgs = z.infer +type DownloadToWorkspaceFileResult = z.infer + +function sanitizeFileName(fileName: string): string { + return fileName.replace(/[\\/:*?"<>|\u0000-\u001f]+/g, '_').trim() +} + +function stripQueryAndHash(input: string): string { + return input.split('#')[0]?.split('?')[0] ?? input +} + +function extractFileNameFromUrl(url: string): string | undefined { + try { + const pathname = new URL(url).pathname + const lastSegment = pathname.split('/').pop() + if (!lastSegment) return undefined + const decoded = decodeURIComponent(lastSegment) + return decoded && decoded !== '/' ? decoded : undefined + } catch { + return undefined + } +} + +function extractFileNameFromContentDisposition(header: string | null): string | undefined { + if (!header) return undefined + + const utf8Match = header.match(/filename\*\s*=\s*UTF-8''([^;]+)/i) + if (utf8Match?.[1]) { + try { + return decodeURIComponent(utf8Match[1].trim()) + } catch { + return utf8Match[1].trim() + } + } + + const quotedMatch = header.match(/filename\s*=\s*"([^"]+)"/i) + if (quotedMatch?.[1]) return quotedMatch[1].trim() + + const bareMatch = header.match(/filename\s*=\s*([^;]+)/i) + if (bareMatch?.[1]) return bareMatch[1].trim() + + return undefined +} + +function resolveMimeType( + responseContentType: string | null, + candidateFileName?: string, + sourceUrl?: string +): string { + const headerMime = responseContentType?.split(';')[0]?.trim().toLowerCase() + if (headerMime && headerMime !== 'application/octet-stream') { + return headerMime + } + + const fileName = candidateFileName || extractFileNameFromUrl(sourceUrl || '') + const ext = fileName ? getFileExtension(stripQueryAndHash(fileName)) : '' + return ext ? getMimeTypeFromExtension(ext) : 'application/octet-stream' +} + +function ensureFileExtension(fileName: string, mimeType: string): string { + const ext = getFileExtension(stripQueryAndHash(fileName)) + if (ext) return fileName + + const inferredExt = getExtensionFromMimeType(mimeType) + return inferredExt ? `${fileName}.${inferredExt}` : fileName +} + +function inferOutputFileName( + requestedFileName: string | undefined, + response: Response, + url: string, + mimeType: string +): string { + const preferredName = + requestedFileName || + extractFileNameFromContentDisposition(response.headers.get('content-disposition')) || + extractFileNameFromUrl(url) || + 'downloaded-file' + + const sanitized = sanitizeFileName(stripQueryAndHash(preferredName)) || 'downloaded-file' + return ensureFileExtension(sanitized, mimeType) +} + +export const downloadToWorkspaceFileServerTool: BaseServerTool< + DownloadToWorkspaceFileArgs, + DownloadToWorkspaceFileResult +> = { + name: 'download_to_workspace_file', + inputSchema: DownloadToWorkspaceFileArgsSchema, + outputSchema: DownloadToWorkspaceFileResultSchema, + + async execute( + params: DownloadToWorkspaceFileArgs, + context?: ServerToolContext + ): Promise { + if (!context?.userId) { + throw new Error('Authentication required') + } + + const workspaceId = context.workspaceId + if (!workspaceId) { + return { success: false, message: 'Workspace ID is required' } + } + + try { + assertServerToolNotAborted(context) + + const response = await fetch(params.url, { + redirect: 'follow', + signal: context.abortSignal, + }) + + if (!response.ok) { + return { + success: false, + message: `Download failed with status ${response.status} ${response.statusText}`, + } + } + + const mimeType = resolveMimeType( + response.headers.get('content-type'), + params.fileName, + response.url || params.url + ) + const fileName = inferOutputFileName( + params.fileName, + response, + response.url || params.url, + mimeType + ) + + assertServerToolNotAborted(context) + + const arrayBuffer = await response.arrayBuffer() + const fileBuffer = Buffer.from(arrayBuffer) + + if (fileBuffer.length === 0) { + return { success: false, message: 'Downloaded file is empty' } + } + + const uploaded = await uploadWorkspaceFile( + workspaceId, + context.userId, + fileBuffer, + fileName, + mimeType + ) + + logger.info('Downloaded remote file to workspace', { + sourceUrl: params.url, + resolvedUrl: response.url, + fileId: uploaded.id, + fileName: uploaded.name, + mimeType, + size: fileBuffer.length, + }) + + return { + success: true, + message: `Downloaded "${uploaded.name}" to workspace (${fileBuffer.length} bytes)`, + fileId: uploaded.id, + fileName: uploaded.name, + downloadUrl: uploaded.url, + } + } catch (error) { + const msg = error instanceof Error ? error.message : 'Unknown error' + logger.error('Failed to download file to workspace', { url: params.url, error: msg }) + return { success: false, message: `Failed to download file: ${msg}` } + } + }, +} diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts index 73f075e0592..26a97d2b995 100644 --- a/apps/sim/lib/copilot/tools/server/router.ts +++ b/apps/sim/lib/copilot/tools/server/router.ts @@ -4,6 +4,7 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' +import { downloadToWorkspaceFileServerTool } from '@/lib/copilot/tools/server/files/download-to-workspace-file' import { getBlocksMetadataServerTool } from '@/lib/copilot/tools/server/blocks/get-blocks-metadata-tool' import { getTriggerBlocksServerTool } from '@/lib/copilot/tools/server/blocks/get-trigger-blocks' import { searchDocumentationServerTool } from '@/lib/copilot/tools/server/docs/search-documentation' @@ -90,6 +91,7 @@ const serverToolRegistry: Record = { [knowledgeBaseServerTool.name]: knowledgeBaseServerTool, [userTableServerTool.name]: userTableServerTool, [workspaceFileServerTool.name]: workspaceFileServerTool, + [downloadToWorkspaceFileServerTool.name]: downloadToWorkspaceFileServerTool, [generateVisualizationServerTool.name]: generateVisualizationServerTool, [generateImageServerTool.name]: generateImageServerTool, } From 77a4f2f4661478d13576b8ccf99ac25e762d9be3 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 17:05:21 -0700 Subject: [PATCH 08/31] Update tools --- .../message-content/message-content.tsx | 51 +++++--- .../[workspaceId]/home/hooks/use-chat.ts | 19 ++- .../app/workspace/[workspaceId]/home/types.ts | 2 + apps/sim/lib/copilot/store-utils.ts | 121 +++++++++++++++++- 4 files changed, 169 insertions(+), 24 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index cfa7adb625a..cfb2af0f8b6 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -2,6 +2,8 @@ import type { ContentBlock, OptionItem, SubagentName, ToolCallData } from '../../types' import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types' +import { resolveToolDisplay } from '@/lib/copilot/store-utils' +import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-display-registry' import type { AgentGroupItem } from './components' import { AgentGroup, ChatContent, CircleStop, Options, PendingTagIndicator } from './components' @@ -53,15 +55,40 @@ function resolveAgentLabel(key: string): string { return SUBAGENT_LABELS[key as SubagentName] ?? formatToolName(key) } +function mapToolStatusToClientState(status: ContentBlock['toolCall'] extends { status: infer T } ? T : string) { + switch (status) { + case 'success': + return ClientToolCallState.success + case 'error': + return ClientToolCallState.error + case 'cancelled': + return ClientToolCallState.cancelled + default: + return ClientToolCallState.executing + } +} + +function getOverrideDisplayTitle(tc: NonNullable): string | undefined { + if (tc.name === 'read' || tc.name.endsWith('_respond')) { + return resolveToolDisplay(tc.name, mapToolStatusToClientState(tc.status), tc.id, tc.params)?.text + } + return undefined +} + function toToolData(tc: NonNullable): ToolCallData { + const overrideDisplayTitle = getOverrideDisplayTitle(tc) + const displayTitle = + overrideDisplayTitle || + tc.displayTitle || + TOOL_UI_METADATA[tc.name as keyof typeof TOOL_UI_METADATA]?.title || + formatToolName(tc.name) + return { id: tc.id, toolName: tc.name, - displayTitle: - tc.displayTitle || - TOOL_UI_METADATA[tc.name as keyof typeof TOOL_UI_METADATA]?.title || - formatToolName(tc.name), + displayTitle, status: tc.status, + params: tc.params, result: tc.result, streamingArgs: tc.streamingArgs, } @@ -81,25 +108,12 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { const block = blocks[i] if (block.type === 'subagent_text') { - if (!block.content || !group) continue - const lastItem = group.items[group.items.length - 1] - if (lastItem?.type === 'text') { - lastItem.content += block.content - } else { - group.items.push({ type: 'text', content: block.content }) - } continue } if (block.type === 'text') { if (!block.content?.trim()) continue - if (block.subagent && group && group.agentName === block.subagent) { - const lastItem = group.items[group.items.length - 1] - if (lastItem?.type === 'text') { - lastItem.content += block.content - } else { - group.items.push({ type: 'text', content: block.content }) - } + if (block.subagent) { continue } if (group) { @@ -148,6 +162,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { if (block.type === 'tool_call') { if (!block.toolCall) continue const tc = block.toolCall + if (tc.name === 'tool_search_tool_regex' || tc.name === 'grep' || tc.name === 'glob') continue const isDispatch = SUBAGENT_KEYS.has(tc.name) && !tc.calledBy if (isDispatch) { 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 0bd9551c8f4..31cbd34bd01 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -97,6 +97,7 @@ function mapStoredBlock(block: TaskStoredContentBlock): ContentBlock { status: resolvedStatus, displayTitle: resolvedStatus === 'cancelled' ? 'Stopped by user' : block.toolCall.display?.text, + params: block.toolCall.params, calledBy: block.toolCall.calledBy, result: block.toolCall.result, } @@ -114,6 +115,7 @@ function mapStoredToolCall(tc: TaskStoredToolCall): ContentBlock { name: tc.name, status: resolvedStatus, displayTitle: resolvedStatus === 'cancelled' ? 'Stopped by user' : undefined, + params: tc.params, result: tc.result != null ? { @@ -736,11 +738,20 @@ export function useChat( const isPartial = data?.partial === true if (!id) break - if (name.endsWith('_respond')) break + if ( + name === 'tool_search_tool_regex' || + name === 'grep' || + name === 'glob' + ) { + break + } const ui = parsed.ui || data?.ui if (ui?.hidden) break const displayTitle = ui?.title || ui?.phaseLabel const phaseLabel = ui?.phaseLabel + const args = (data?.arguments ?? data?.input) as + | Record + | undefined if (!toolMap.has(id)) { toolMap.set(id, blocks.length) blocks.push({ @@ -751,13 +762,11 @@ export function useChat( status: 'executing', displayTitle, phaseLabel, + params: args, calledBy: activeSubagent, }, }) if (name === 'read' || isResourceToolName(name)) { - const args = (data?.arguments ?? data?.input) as - | Record - | undefined if (args) toolArgsMap.set(id, args) } } else { @@ -767,6 +776,7 @@ export function useChat( tc.name = name if (displayTitle) tc.displayTitle = displayTitle if (phaseLabel) tc.phaseLabel = phaseLabel + if (args) tc.params = args } } flush() @@ -1140,6 +1150,7 @@ export function useChat( id: block.toolCall.id, name: block.toolCall.name, state: isCancelled ? 'cancelled' : block.toolCall.status, + params: block.toolCall.params, result: block.toolCall.result, display: { text: isCancelled ? 'Stopped by user' : block.toolCall.displayTitle, diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index 21c6c295c40..0ba5c32ec78 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -145,6 +145,7 @@ export interface ToolCallData { toolName: string displayTitle: string status: ToolCallStatus + params?: Record result?: ToolCallResult streamingArgs?: string } @@ -155,6 +156,7 @@ export interface ToolCallInfo { status: ToolCallStatus displayTitle?: string phaseLabel?: string + params?: Record calledBy?: string result?: { success: boolean; output?: unknown; error?: string } streamingArgs?: string diff --git a/apps/sim/lib/copilot/store-utils.ts b/apps/sim/lib/copilot/store-utils.ts index ad4642dd2c8..750a2c07baf 100644 --- a/apps/sim/lib/copilot/store-utils.ts +++ b/apps/sim/lib/copilot/store-utils.ts @@ -28,11 +28,13 @@ import { type ClientToolDisplay, TOOL_DISPLAY_REGISTRY, } from '@/lib/copilot/tools/client/tool-display-registry' +import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resource-types' const logger = createLogger('CopilotStoreUtils') -/** Respond tools are internal to copilot subagents and should never be shown in the UI */ +/** Respond tools are internal handoff tools shown with a friendly generic label. */ const HIDDEN_TOOL_SUFFIX = '_respond' +const HIDDEN_TOOL_NAMES = new Set(['tool_search_tool_regex', 'grep', 'glob']) /** UI metadata sent by the copilot on SSE tool_call events. */ export interface ServerToolUI { @@ -81,7 +83,11 @@ export function resolveToolDisplay( serverUI?: ServerToolUI ): ClientToolDisplay | undefined { if (!toolName) return undefined - if (toolName.endsWith(HIDDEN_TOOL_SUFFIX)) return undefined + if (HIDDEN_TOOL_NAMES.has(toolName)) return undefined + + const specialDisplay = specialToolDisplay(toolName, state, params) + if (specialDisplay) return specialDisplay + const entry = TOOL_DISPLAY_REGISTRY[toolName] if (!entry) { // Use copilot-provided UI as a better fallback than humanized name @@ -115,6 +121,117 @@ export function resolveToolDisplay( return humanizedFallback(toolName, state) } +function specialToolDisplay( + toolName: string, + state: ClientToolCallState, + params?: Record +): ClientToolDisplay | undefined { + if (toolName.endsWith(HIDDEN_TOOL_SUFFIX)) { + return { + text: formatRespondLabel(state), + icon: Loader2, + } + } + + const searchQuery = + readStringParam(params, 'pattern') || readStringParam(params, 'query') || readStringParam(params, 'glob') + + if ((toolName === 'grep' || toolName === 'glob') && searchQuery) { + return { + text: formatSearchingLabel(searchQuery, state), + icon: Search, + } + } + + if (toolName === 'read') { + const target = describeReadTarget(readStringParam(params, 'path')) + return { + text: formatReadingLabel(target, state), + icon: FileText, + } + } + + return undefined +} + +function formatRespondLabel(state: ClientToolCallState): string { + switch (state) { + case ClientToolCallState.success: + return 'Returned results' + case ClientToolCallState.error: + return 'Failed returning results' + case ClientToolCallState.rejected: + case ClientToolCallState.aborted: + return 'Skipped returning results' + default: + return 'Returning results' + } +} + +function readStringParam( + params: Record | undefined, + key: string +): string | undefined { + const value = params?.[key] + return typeof value === 'string' && value.trim() ? value.trim() : undefined +} + +function formatSearchingLabel(query: string, state: ClientToolCallState): string { + switch (state) { + case ClientToolCallState.success: + return `Searched for ${query}` + case ClientToolCallState.error: + return `Failed searching for ${query}` + case ClientToolCallState.rejected: + case ClientToolCallState.aborted: + return `Skipped searching for ${query}` + default: + return `Searching for ${query}` + } +} + +function formatReadingLabel(target: string | undefined, state: ClientToolCallState): string { + const suffix = target ? ` ${target}` : '' + switch (state) { + case ClientToolCallState.success: + return `Read${suffix}` + case ClientToolCallState.error: + return `Failed reading${suffix}` + case ClientToolCallState.rejected: + case ClientToolCallState.aborted: + return `Skipped reading${suffix}` + default: + return `Reading${suffix}` + } +} + +function describeReadTarget(path: string | undefined): string | undefined { + if (!path) return undefined + + const segments = path + .split('/') + .map((segment) => segment.trim()) + .filter(Boolean) + + if (segments.length === 0) return undefined + + const resourceType = VFS_DIR_TO_RESOURCE[segments[0]] + if (!resourceType) { + return stripExtension(segments[segments.length - 1]) + } + + if (resourceType === 'file') { + return segments.slice(1).join('/') || segments[segments.length - 1] + } + + const resourceName = segments[1] || segments[segments.length - 1] + return stripExtension(resourceName) +} + +function stripExtension(value: string): string { + return value.replace(/\.[^/.]+$/, '') +} + /** Generates display from copilot-provided UI metadata. */ function serverUIFallback(serverUI: ServerToolUI, state: ClientToolCallState): ClientToolDisplay { const icon = resolveIcon(serverUI.icon) From d07124841a50116d1847b72f2dbd0c234aa559ff Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 17:05:33 -0700 Subject: [PATCH 09/31] Fix lint --- .../components/message-content/message-content.tsx | 11 +++++++---- .../workspace/[workspaceId]/home/hooks/use-chat.ts | 10 ++-------- apps/sim/lib/copilot/store-utils.ts | 6 ++++-- .../tools/server/files/download-to-workspace-file.ts | 2 +- apps/sim/lib/copilot/tools/server/router.ts | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index cfb2af0f8b6..a9288d38ce8 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -1,9 +1,9 @@ 'use client' -import type { ContentBlock, OptionItem, SubagentName, ToolCallData } from '../../types' -import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types' import { resolveToolDisplay } from '@/lib/copilot/store-utils' import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-display-registry' +import type { ContentBlock, OptionItem, SubagentName, ToolCallData } from '../../types' +import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types' import type { AgentGroupItem } from './components' import { AgentGroup, ChatContent, CircleStop, Options, PendingTagIndicator } from './components' @@ -55,7 +55,9 @@ function resolveAgentLabel(key: string): string { return SUBAGENT_LABELS[key as SubagentName] ?? formatToolName(key) } -function mapToolStatusToClientState(status: ContentBlock['toolCall'] extends { status: infer T } ? T : string) { +function mapToolStatusToClientState( + status: ContentBlock['toolCall'] extends { status: infer T } ? T : string +) { switch (status) { case 'success': return ClientToolCallState.success @@ -70,7 +72,8 @@ function mapToolStatusToClientState(status: ContentBlock['toolCall'] extends { s function getOverrideDisplayTitle(tc: NonNullable): string | undefined { if (tc.name === 'read' || tc.name.endsWith('_respond')) { - return resolveToolDisplay(tc.name, mapToolStatusToClientState(tc.status), tc.id, tc.params)?.text + return resolveToolDisplay(tc.name, mapToolStatusToClientState(tc.status), tc.id, tc.params) + ?.text } return undefined } 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 31cbd34bd01..5be537d6147 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -738,20 +738,14 @@ export function useChat( const isPartial = data?.partial === true if (!id) break - if ( - name === 'tool_search_tool_regex' || - name === 'grep' || - name === 'glob' - ) { + if (name === 'tool_search_tool_regex' || name === 'grep' || name === 'glob') { break } const ui = parsed.ui || data?.ui if (ui?.hidden) break const displayTitle = ui?.title || ui?.phaseLabel const phaseLabel = ui?.phaseLabel - const args = (data?.arguments ?? data?.input) as - | Record - | undefined + const args = (data?.arguments ?? data?.input) as Record | undefined if (!toolMap.has(id)) { toolMap.set(id, blocks.length) blocks.push({ diff --git a/apps/sim/lib/copilot/store-utils.ts b/apps/sim/lib/copilot/store-utils.ts index 750a2c07baf..ec867702207 100644 --- a/apps/sim/lib/copilot/store-utils.ts +++ b/apps/sim/lib/copilot/store-utils.ts @@ -23,12 +23,12 @@ import { Wrench, Zap, } from 'lucide-react' +import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resource-types' import { ClientToolCallState, type ClientToolDisplay, TOOL_DISPLAY_REGISTRY, } from '@/lib/copilot/tools/client/tool-display-registry' -import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resource-types' const logger = createLogger('CopilotStoreUtils') @@ -134,7 +134,9 @@ function specialToolDisplay( } const searchQuery = - readStringParam(params, 'pattern') || readStringParam(params, 'query') || readStringParam(params, 'glob') + readStringParam(params, 'pattern') || + readStringParam(params, 'query') || + readStringParam(params, 'glob') if ((toolName === 'grep' || toolName === 'glob') && searchQuery) { return { diff --git a/apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts index fe128473784..dab810ab72d 100644 --- a/apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts @@ -5,12 +5,12 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' +import { uploadWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { getExtensionFromMimeType, getFileExtension, getMimeTypeFromExtension, } from '@/lib/uploads/utils/file-utils' -import { uploadWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager' const logger = createLogger('DownloadToWorkspaceFileTool') diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts index 26a97d2b995..83e6425719b 100644 --- a/apps/sim/lib/copilot/tools/server/router.ts +++ b/apps/sim/lib/copilot/tools/server/router.ts @@ -4,10 +4,10 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' -import { downloadToWorkspaceFileServerTool } from '@/lib/copilot/tools/server/files/download-to-workspace-file' import { getBlocksMetadataServerTool } from '@/lib/copilot/tools/server/blocks/get-blocks-metadata-tool' import { getTriggerBlocksServerTool } from '@/lib/copilot/tools/server/blocks/get-trigger-blocks' import { searchDocumentationServerTool } from '@/lib/copilot/tools/server/docs/search-documentation' +import { downloadToWorkspaceFileServerTool } from '@/lib/copilot/tools/server/files/download-to-workspace-file' import { workspaceFileServerTool } from '@/lib/copilot/tools/server/files/workspace-file' import { generateImageServerTool } from '@/lib/copilot/tools/server/image/generate-image' import { getJobLogsServerTool } from '@/lib/copilot/tools/server/jobs/get-job-logs' From 844c9a284b88c610bd699d3bd965c11d10d89164 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 17:07:17 -0700 Subject: [PATCH 10/31] Fix error msg --- .../components/special-tags/special-tags.tsx | 31 +++---------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 6857fc7807f..4513f95d56e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -449,33 +449,12 @@ function CredentialDisplay({ data }: { data: CredentialTagData }) { } function MothershipErrorDisplay({ data }: { data: MothershipErrorTagData }) { + const detail = data.code ? `${data.message} (${data.code})` : data.message + return ( -
-
- - - - - - - Something went wrong - -
-

- {data.message} -

- {data.code && ( - - {data.provider ? `${data.provider}:` : ''} - {data.code} - - )} -
+ + {detail} + ) } From 46e8964f2415c30099315c9d6edb045748664b4f Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 17:21:17 -0700 Subject: [PATCH 11/31] Tool fixes --- .../message-content/message-content.tsx | 2 +- .../[workspaceId]/home/hooks/use-chat.ts | 2 +- apps/sim/lib/copilot/store-utils.ts | 28 +------------------ 3 files changed, 3 insertions(+), 29 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index a9288d38ce8..01285a1a290 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -165,7 +165,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { if (block.type === 'tool_call') { if (!block.toolCall) continue const tc = block.toolCall - if (tc.name === 'tool_search_tool_regex' || tc.name === 'grep' || tc.name === 'glob') continue + if (tc.name === 'tool_search_tool_regex') continue const isDispatch = SUBAGENT_KEYS.has(tc.name) && !tc.calledBy if (isDispatch) { 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 5be537d6147..bb26e7397a3 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -738,7 +738,7 @@ export function useChat( const isPartial = data?.partial === true if (!id) break - if (name === 'tool_search_tool_regex' || name === 'grep' || name === 'glob') { + if (name === 'tool_search_tool_regex') { break } const ui = parsed.ui || data?.ui diff --git a/apps/sim/lib/copilot/store-utils.ts b/apps/sim/lib/copilot/store-utils.ts index ec867702207..9447f9cbf11 100644 --- a/apps/sim/lib/copilot/store-utils.ts +++ b/apps/sim/lib/copilot/store-utils.ts @@ -34,7 +34,7 @@ const logger = createLogger('CopilotStoreUtils') /** Respond tools are internal handoff tools shown with a friendly generic label. */ const HIDDEN_TOOL_SUFFIX = '_respond' -const HIDDEN_TOOL_NAMES = new Set(['tool_search_tool_regex', 'grep', 'glob']) +const HIDDEN_TOOL_NAMES = new Set(['tool_search_tool_regex']) /** UI metadata sent by the copilot on SSE tool_call events. */ export interface ServerToolUI { @@ -133,18 +133,6 @@ function specialToolDisplay( } } - const searchQuery = - readStringParam(params, 'pattern') || - readStringParam(params, 'query') || - readStringParam(params, 'glob') - - if ((toolName === 'grep' || toolName === 'glob') && searchQuery) { - return { - text: formatSearchingLabel(searchQuery, state), - icon: Search, - } - } - if (toolName === 'read') { const target = describeReadTarget(readStringParam(params, 'path')) return { @@ -178,20 +166,6 @@ function readStringParam( return typeof value === 'string' && value.trim() ? value.trim() : undefined } -function formatSearchingLabel(query: string, state: ClientToolCallState): string { - switch (state) { - case ClientToolCallState.success: - return `Searched for ${query}` - case ClientToolCallState.error: - return `Failed searching for ${query}` - case ClientToolCallState.rejected: - case ClientToolCallState.aborted: - return `Skipped searching for ${query}` - default: - return `Searching for ${query}` - } -} - function formatReadingLabel(target: string | undefined, state: ClientToolCallState): string { const suffix = target ? ` ${target}` : '' switch (state) { From 6549a50a83d1fe276eb595d1b4a36a6e1afecb53 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 18:07:01 -0700 Subject: [PATCH 12/31] Reenable subagent stream --- .../components/agent-group/agent-group.tsx | 12 ++++++------ .../message-content/message-content.tsx | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index 56c7b979889..a8f73e0e9a1 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -5,6 +5,7 @@ import { ChevronDown } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import type { ToolCallData } from '../../../../types' import { getAgentIcon } from '../../utils' +import { ChatContent } from '../chat-content/chat-content' import { ToolCallItem } from './tool-call-item' export type AgentGroupItem = @@ -16,6 +17,7 @@ interface AgentGroupProps { agentLabel: string items: AgentGroupItem[] autoCollapse?: boolean + isStreaming?: boolean } const FADE_MS = 300 @@ -25,6 +27,7 @@ export function AgentGroup({ agentLabel, items, autoCollapse = false, + isStreaming = false, }: AgentGroupProps) { const AgentIcon = getAgentIcon(agentName) const hasItems = items.length > 0 @@ -100,12 +103,9 @@ export function AgentGroup({ status={item.data.status} /> ) : ( - - {item.content.trim()} - +
+ +
) )}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 01285a1a290..68c2206639b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -111,13 +111,28 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { const block = blocks[i] if (block.type === 'subagent_text') { + if (!block.content || !group) continue + const lastItem = group.items[group.items.length - 1] + if (lastItem?.type === 'text') { + lastItem.content += block.content + } else { + group.items.push({ type: 'text', content: block.content }) + } continue } if (block.type === 'text') { if (!block.content?.trim()) continue if (block.subagent) { - continue + if (group && group.agentName === block.subagent) { + const lastItem = group.items[group.items.length - 1] + if (lastItem?.type === 'text') { + lastItem.content += block.content + } else { + group.items.push({ type: 'text', content: block.content }) + } + continue + } } if (group) { segments.push(group) @@ -358,6 +373,7 @@ export function MessageContent({ agentLabel={segment.agentLabel} items={segment.items} autoCollapse={allToolsDone && hasFollowingText} + isStreaming={isStreaming} /> ) From 98f4dfda93ade7451092a6e9879cad7469c41c01 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 18:12:03 -0700 Subject: [PATCH 13/31] Subagent stream --- .../components/agent-group/agent-group.tsx | 12 ++++++------ .../components/message-content/message-content.tsx | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index a8f73e0e9a1..56c7b979889 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -5,7 +5,6 @@ import { ChevronDown } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import type { ToolCallData } from '../../../../types' import { getAgentIcon } from '../../utils' -import { ChatContent } from '../chat-content/chat-content' import { ToolCallItem } from './tool-call-item' export type AgentGroupItem = @@ -17,7 +16,6 @@ interface AgentGroupProps { agentLabel: string items: AgentGroupItem[] autoCollapse?: boolean - isStreaming?: boolean } const FADE_MS = 300 @@ -27,7 +25,6 @@ export function AgentGroup({ agentLabel, items, autoCollapse = false, - isStreaming = false, }: AgentGroupProps) { const AgentIcon = getAgentIcon(agentName) const hasItems = items.length > 0 @@ -103,9 +100,12 @@ export function AgentGroup({ status={item.data.status} /> ) : ( -
- -
+ + {item.content.trim()} + ) )} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 68c2206639b..afe1bd7819d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -373,7 +373,6 @@ export function MessageContent({ agentLabel={segment.agentLabel} items={segment.items} autoCollapse={allToolsDone && hasFollowingText} - isStreaming={isStreaming} /> ) From 1e7a98741b093d675d997ee30b2e8a73159f4f36 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 18:20:24 -0700 Subject: [PATCH 14/31] Fix edit workflow hydration --- .../app/workspace/[workspaceId]/home/hooks/use-chat.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 bb26e7397a3..706c104f6a3 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -991,19 +991,15 @@ export function useChat( onResourceEventRef.current?.() if (resource.type === 'workflow') { - const registry = useWorkflowRegistry.getState() const wasRegistered = ensureWorkflowInRegistry( resource.id, resource.title, workspaceId ) if (wasAdded && wasRegistered) { - registry.setActiveWorkflow(resource.id) - } else if ( - registry.activeWorkflowId !== resource.id || - registry.hydration.phase !== 'ready' - ) { - registry.loadWorkflowState(resource.id) + useWorkflowRegistry.getState().setActiveWorkflow(resource.id) + } else { + useWorkflowRegistry.getState().loadWorkflowState(resource.id) } } } From 0b3000ad1e36b87ea9355eae8e6b43cb32aef3a1 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 18:50:03 -0700 Subject: [PATCH 15/31] Throw func execute error on error --- .../components/file-viewer/file-viewer.tsx | 47 ++++++++++++++++++- .../orchestrator/tool-executor/index.ts | 21 +++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index 04dd5d9725e..fa046e79867 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -457,8 +457,9 @@ async function renderPptxSlides( if (cancelled()) return const dpr = Math.min(window.devicePixelRatio || 1, 2) - const W = Math.round(1920 * dpr) - const H = Math.round(1080 * dpr) + const { width, height } = await getPptxRenderSize(data, dpr) + const W = width + const H = height const canvas = document.createElement('canvas') canvas.width = W @@ -476,6 +477,48 @@ async function renderPptxSlides( } } +async function getPptxRenderSize( + data: Uint8Array, + dpr: number +): Promise<{ width: number; height: number }> { + const fallback = { + width: Math.round(1920 * dpr), + height: Math.round(1080 * dpr), + } + + try { + const JSZip = (await import('jszip')).default + const zip = await JSZip.loadAsync(data) + const presentationXml = await zip.file('ppt/presentation.xml')?.async('text') + if (!presentationXml) return fallback + + const match = presentationXml.match(/]*cx="(\d+)"[^>]*cy="(\d+)"/) + if (!match) return fallback + + const cx = Number(match[1]) + const cy = Number(match[2]) + if (!Number.isFinite(cx) || !Number.isFinite(cy) || cx <= 0 || cy <= 0) return fallback + + const aspectRatio = cx / cy + if (!Number.isFinite(aspectRatio) || aspectRatio <= 0) return fallback + + const baseLongEdge = 1920 * dpr + if (aspectRatio >= 1) { + return { + width: Math.round(baseLongEdge), + height: Math.round(baseLongEdge / aspectRatio), + } + } + + return { + width: Math.round(baseLongEdge * aspectRatio), + height: Math.round(baseLongEdge), + } + } catch { + return fallback + } +} + function PptxPreview({ file, workspaceId, diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts index d5dfcb0754b..db8f430c0d7 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts @@ -1243,6 +1243,27 @@ async function executeServerToolDirect( chatId: context.chatId, abortSignal: context.abortSignal, }) + + const resultRecord = + result && typeof result === 'object' && !Array.isArray(result) + ? (result as Record) + : null + + // Some server tools return an explicit { success, message, ... } envelope. + // Preserve tool-level failures instead of reporting them as transport success. + if (resultRecord?.success === false) { + const message = + (typeof resultRecord.error === 'string' && resultRecord.error) || + (typeof resultRecord.message === 'string' && resultRecord.message) || + `${toolName} failed` + + return { + success: false, + error: message, + output: result, + } + } + return { success: true, output: result } } catch (error) { logger.error('Server tool execution failed', { From e2d5d27a69c342a4643ca5d01815348a67000951 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 22 Mar 2026 20:21:55 -0700 Subject: [PATCH 16/31] Rewrite --- apps/sim/app/api/copilot/confirm/route.ts | 44 +++++++-- apps/sim/lib/copilot/async-runs/lifecycle.ts | 45 +++++++++ apps/sim/lib/copilot/async-runs/repository.ts | 40 +++++--- apps/sim/lib/copilot/orchestrator/index.ts | 88 ++++++++++++++++- .../lib/copilot/orchestrator/persistence.ts | 94 +++++++++++++++++++ .../orchestrator/sse/handlers/handlers.ts | 22 ++--- .../sse/handlers/tool-execution.ts | 70 +++----------- .../copilot/orchestrator/stream/core.test.ts | 63 +++++++++++++ .../lib/copilot/orchestrator/stream/core.ts | 4 +- 9 files changed, 371 insertions(+), 99 deletions(-) create mode 100644 apps/sim/lib/copilot/async-runs/lifecycle.ts create mode 100644 apps/sim/lib/copilot/orchestrator/stream/core.test.ts diff --git a/apps/sim/app/api/copilot/confirm/route.ts b/apps/sim/app/api/copilot/confirm/route.ts index fd03b83533d..20844491ef8 100644 --- a/apps/sim/app/api/copilot/confirm/route.ts +++ b/apps/sim/app/api/copilot/confirm/route.ts @@ -1,8 +1,13 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { completeAsyncToolCall, upsertAsyncToolCall } from '@/lib/copilot/async-runs/repository' +import { + completeAsyncToolCall, + getAsyncToolCall, + upsertAsyncToolCall, +} from '@/lib/copilot/async-runs/repository' import { REDIS_TOOL_CALL_PREFIX, REDIS_TOOL_CALL_TTL_SECONDS } from '@/lib/copilot/constants' +import { publishToolConfirmation } from '@/lib/copilot/orchestrator/persistence' import { authenticateCopilotRequestSessionOnly, createBadRequestResponse, @@ -43,13 +48,22 @@ async function updateToolCallStatus( : status === 'error' || status === 'rejected' ? 'failed' : 'pending' - await upsertAsyncToolCall({ - runId: crypto.randomUUID(), - toolCallId, - toolName: 'client_tool', - args: {}, - status: durableStatus, - }).catch(() => {}) + const existing = await getAsyncToolCall(toolCallId).catch(() => null) + if (existing?.runId) { + await upsertAsyncToolCall({ + runId: existing.runId, + checkpointId: existing.checkpointId ?? null, + toolCallId, + toolName: existing.toolName || 'client_tool', + args: (existing.args as Record | null) ?? {}, + status: durableStatus, + }).catch(() => {}) + } else { + logger.warn('Tool confirmation has no existing async tool row; durable state may be missing', { + toolCallId, + status, + }) + } if ( durableStatus === 'completed' || durableStatus === 'failed' || @@ -66,6 +80,13 @@ async function updateToolCallStatus( const redis = getRedisClient() if (!redis) { logger.warn('Redis client not available for tool confirmation; durable DB mirror only') + publishToolConfirmation({ + toolCallId, + status, + message: message || undefined, + timestamp: new Date().toISOString(), + data, + }) return true } @@ -80,6 +101,13 @@ async function updateToolCallStatus( payload.data = data } await redis.set(key, JSON.stringify(payload), 'EX', REDIS_TOOL_CALL_TTL_SECONDS) + publishToolConfirmation({ + toolCallId, + status, + message: message || undefined, + timestamp: payload.timestamp as string, + data, + }) return true } catch (error) { logger.error('Failed to update tool call status', { diff --git a/apps/sim/lib/copilot/async-runs/lifecycle.ts b/apps/sim/lib/copilot/async-runs/lifecycle.ts new file mode 100644 index 00000000000..47bb77809a7 --- /dev/null +++ b/apps/sim/lib/copilot/async-runs/lifecycle.ts @@ -0,0 +1,45 @@ +import type { CopilotAsyncToolStatus } from '@sim/db/schema' + +export const ASYNC_TOOL_STATUS = { + pending: 'pending', + running: 'running', + completed: 'completed', + failed: 'failed', + cancelled: 'cancelled', + resumeEnqueued: 'resume_enqueued', + resumed: 'resumed', +} as const + +export type AsyncLifecycleStatus = + | typeof ASYNC_TOOL_STATUS.pending + | typeof ASYNC_TOOL_STATUS.running + | typeof ASYNC_TOOL_STATUS.completed + | typeof ASYNC_TOOL_STATUS.failed + | typeof ASYNC_TOOL_STATUS.cancelled + +export type AsyncTerminalStatus = + | typeof ASYNC_TOOL_STATUS.completed + | typeof ASYNC_TOOL_STATUS.failed + | typeof ASYNC_TOOL_STATUS.cancelled + +export interface AsyncCompletionEnvelope { + toolCallId: string + status: string + message?: string + data?: Record + runId?: string + checkpointId?: string + executionId?: string + chatId?: string + timestamp?: string +} + +export function isTerminalAsyncStatus( + status: CopilotAsyncToolStatus | AsyncLifecycleStatus | string | null | undefined +): status is AsyncTerminalStatus { + return ( + status === ASYNC_TOOL_STATUS.completed || + status === ASYNC_TOOL_STATUS.failed || + status === ASYNC_TOOL_STATUS.cancelled + ) +} diff --git a/apps/sim/lib/copilot/async-runs/repository.ts b/apps/sim/lib/copilot/async-runs/repository.ts index ef33f170b92..20fb91ba936 100644 --- a/apps/sim/lib/copilot/async-runs/repository.ts +++ b/apps/sim/lib/copilot/async-runs/repository.ts @@ -8,6 +8,7 @@ import { } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, desc, eq, inArray, isNull } from 'drizzle-orm' +import { isTerminalAsyncStatus } from './lifecycle' const logger = createLogger('CopilotAsyncRunsRepo') @@ -107,18 +108,29 @@ export async function createRunCheckpoint(input: { } export async function upsertAsyncToolCall(input: { - runId: string + runId?: string | null checkpointId?: string | null toolCallId: string toolName: string args?: Record status?: CopilotAsyncToolStatus }) { + const existing = await getAsyncToolCall(input.toolCallId) + const effectiveRunId = input.runId ?? existing?.runId ?? null + if (!effectiveRunId) { + logger.warn('upsertAsyncToolCall missing runId and no existing row', { + toolCallId: input.toolCallId, + toolName: input.toolName, + status: input.status ?? 'pending', + }) + return null + } + const now = new Date() const [row] = await db .insert(copilotAsyncToolCalls) .values({ - runId: input.runId, + runId: effectiveRunId, checkpointId: input.checkpointId ?? null, toolCallId: input.toolCallId, toolName: input.toolName, @@ -129,7 +141,7 @@ export async function upsertAsyncToolCall(input: { .onConflictDoUpdate({ target: copilotAsyncToolCalls.toolCallId, set: { - runId: input.runId, + runId: effectiveRunId, checkpointId: input.checkpointId ?? null, toolName: input.toolName, args: input.args ?? {}, @@ -142,6 +154,16 @@ export async function upsertAsyncToolCall(input: { return row } +export async function getAsyncToolCall(toolCallId: string) { + const [row] = await db + .select() + .from(copilotAsyncToolCalls) + .where(eq(copilotAsyncToolCalls.toolCallId, toolCallId)) + .limit(1) + + return row ?? null +} + export async function markAsyncToolStatus( toolCallId: string, status: CopilotAsyncToolStatus, @@ -186,11 +208,7 @@ export async function completeAsyncToolCall(input: { result?: Record | null error?: string | null }) { - const [existing] = await db - .select() - .from(copilotAsyncToolCalls) - .where(eq(copilotAsyncToolCalls.toolCallId, input.toolCallId)) - .limit(1) + const existing = await getAsyncToolCall(input.toolCallId) if (!existing) { logger.warn('completeAsyncToolCall called before pending row existed', { @@ -200,11 +218,7 @@ export async function completeAsyncToolCall(input: { return null } - if ( - existing.status === 'completed' || - existing.status === 'failed' || - existing.status === 'cancelled' - ) { + if (isTerminalAsyncStatus(existing.status)) { return existing } diff --git a/apps/sim/lib/copilot/orchestrator/index.ts b/apps/sim/lib/copilot/orchestrator/index.ts index 78d966e953c..0c7387dd04a 100644 --- a/apps/sim/lib/copilot/orchestrator/index.ts +++ b/apps/sim/lib/copilot/orchestrator/index.ts @@ -1,5 +1,11 @@ import { createLogger } from '@sim/logger' -import { updateRunStatus } from '@/lib/copilot/async-runs/repository' +import { ASYNC_TOOL_STATUS, isTerminalAsyncStatus } from '@/lib/copilot/async-runs/lifecycle' +import { + claimCompletedAsyncToolCall, + getAsyncToolCalls, + markAsyncToolResumed, + updateRunStatus, +} from '@/lib/copilot/async-runs/repository' import { SIM_AGENT_API_URL, SIM_AGENT_VERSION } from '@/lib/copilot/constants' import { prepareExecutionContext } from '@/lib/copilot/orchestrator/tool-executor' import type { @@ -73,6 +79,7 @@ export async function orchestrateCopilotStream( try { let route = goRoute let payload = requestPayload + let claimedToolCallIds: string[] = [] const callerOnEvent = options.onEvent @@ -120,21 +127,86 @@ export async function orchestrateCopilotStream( const continuation = context.awaitingAsyncContinuation if (!continuation) break + claimedToolCallIds = [] + const resumeWorkerId = continuation.runId || context.runId || context.messageId + const claimableToolCallIds: string[] = [] + for (const toolCallId of continuation.pendingToolCallIds) { + const claimed = await claimCompletedAsyncToolCall(toolCallId, resumeWorkerId).catch( + () => null + ) + if (claimed) { + claimableToolCallIds.push(toolCallId) + claimedToolCallIds.push(toolCallId) + continue + } + // Fall back to local continuation if the durable row is missing, but do not + // retry rows another worker already claimed. + const localPending = context.pendingToolPromises.has(toolCallId) + if (localPending) { + claimableToolCallIds.push(toolCallId) + } else { + logger.warn('Skipping already-claimed or missing async tool resume', { + toolCallId, + runId: continuation.runId, + }) + } + } + + if (claimableToolCallIds.length === 0) { + logger.warn('Skipping async resume because no tool calls were claimable', { + checkpointId: continuation.checkpointId, + runId: continuation.runId, + }) + context.awaitingAsyncContinuation = undefined + break + } + + logger.info('Resuming async tool continuation', { + checkpointId: continuation.checkpointId, + runId: continuation.runId, + toolCallIds: claimableToolCallIds, + }) + + const durableRows = await getAsyncToolCalls(claimableToolCallIds).catch(() => []) + const durableByToolCallId = new Map(durableRows.map((row) => [row.toolCallId, row])) + const results = await Promise.all( - continuation.pendingToolCallIds.map(async (toolCallId) => { + claimableToolCallIds.map(async (toolCallId) => { const completion = await context.pendingToolPromises.get(toolCallId) const toolState = context.toolCalls.get(toolCallId) + + const durable = durableByToolCallId.get(toolCallId) + const durableStatus = durable?.status const success = - completion?.status === 'success' || (!completion && toolState?.result?.success === true) + durableStatus === ASYNC_TOOL_STATUS.completed || + (durableStatus == null && + (completion?.status === 'success' || + (!completion && toolState?.result?.success === true))) + + const durableResult = + durable?.result && typeof durable.result === 'object' + ? (durable.result as Record) + : undefined const data = + durableResult || completion?.data || (toolState?.result?.output as Record | undefined) || (success ? { message: completion?.message || 'Tool completed' } - : { error: completion?.message || toolState?.error || 'Tool failed' }) + : { + error: completion?.message || durable?.error || toolState?.error || 'Tool failed', + }) + + if (durableStatus && !isTerminalAsyncStatus(durableStatus)) { + logger.warn('Async tool row was claimed for resume without terminal durable state', { + toolCallId, + status: durableStatus, + }) + } + return { callId: toolCallId, - name: toolState?.name || '', + name: durable?.toolName || toolState?.name || '', data, success, } @@ -160,6 +232,12 @@ export async function orchestrateCopilotStream( usage: context.usage, cost: context.cost, } + if (result.success && claimedToolCallIds.length > 0) { + logger.info('Marking async tool calls as resumed', { toolCallIds: claimedToolCallIds }) + await Promise.all( + claimedToolCallIds.map((toolCallId) => markAsyncToolResumed(toolCallId).catch(() => null)) + ) + } await options.onComplete?.(result) return result } catch (error) { diff --git a/apps/sim/lib/copilot/orchestrator/persistence.ts b/apps/sim/lib/copilot/orchestrator/persistence.ts index 5c207d8fc12..8dfc4075169 100644 --- a/apps/sim/lib/copilot/orchestrator/persistence.ts +++ b/apps/sim/lib/copilot/orchestrator/persistence.ts @@ -1,10 +1,17 @@ import { createLogger } from '@sim/logger' +import type { AsyncCompletionEnvelope } from '@/lib/copilot/async-runs/lifecycle' import { getAsyncToolCalls } from '@/lib/copilot/async-runs/repository' import { REDIS_TOOL_CALL_PREFIX } from '@/lib/copilot/constants' import { getRedisClient } from '@/lib/core/config/redis' +import { createPubSubChannel } from '@/lib/events/pubsub' const logger = createLogger('CopilotOrchestratorPersistence') +const toolConfirmationChannel = createPubSubChannel({ + channel: 'copilot:tool-confirmation', + label: 'CopilotToolConfirmation', +}) + /** * Get a tool call confirmation status from Redis. */ @@ -43,3 +50,90 @@ export async function getToolConfirmation(toolCallId: string): Promise<{ return null } } + +export function publishToolConfirmation(event: AsyncCompletionEnvelope): void { + logger.info('Publishing tool confirmation event', { + toolCallId: event.toolCallId, + status: event.status, + }) + toolConfirmationChannel.publish(event) +} + +export async function waitForToolConfirmation( + toolCallId: string, + timeoutMs: number, + abortSignal?: AbortSignal +): Promise<{ + status: string + message?: string + timestamp?: string + data?: Record +} | null> { + const existing = await getToolConfirmation(toolCallId) + if (existing) { + logger.info('Resolved tool confirmation immediately', { + toolCallId, + status: existing.status, + }) + return existing + } + + return new Promise((resolve) => { + let settled = false + let timeoutId: ReturnType | null = null + let unsubscribe: (() => void) | null = null + + const cleanup = () => { + if (timeoutId) clearTimeout(timeoutId) + if (unsubscribe) unsubscribe() + abortSignal?.removeEventListener('abort', onAbort) + } + + const settle = ( + value: { + status: string + message?: string + timestamp?: string + data?: Record + } | null + ) => { + if (settled) return + settled = true + cleanup() + resolve(value) + } + + const onAbort = () => settle(null) + + unsubscribe = toolConfirmationChannel.subscribe((event) => { + if (event.toolCallId !== toolCallId) return + logger.info('Resolved tool confirmation from pubsub', { + toolCallId, + status: event.status, + }) + settle({ + status: event.status, + message: event.message, + timestamp: event.timestamp, + data: event.data, + }) + }) + + timeoutId = setTimeout(() => settle(null), timeoutMs) + if (abortSignal?.aborted) { + settle(null) + return + } + abortSignal?.addEventListener('abort', onAbort, { once: true }) + + void getToolConfirmation(toolCallId).then((latest) => { + if (latest) { + logger.info('Resolved tool confirmation after subscribe', { + toolCallId, + status: latest.status, + }) + settle(latest) + } + }) + }) +} diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts index e2354a6eeed..20caba84e01 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts @@ -174,7 +174,13 @@ async function emitSyntheticToolResult( result: resultPayload, error: !success ? completion?.message : undefined, } as SSEEvent) - } catch {} + } catch (error) { + logger.warn('Failed to emit synthetic tool_result', { + toolCallId, + toolName, + error: error instanceof Error ? error.message : String(error), + }) + } } // Normalization + dedupe helpers live in sse-utils to keep server/client in sync. @@ -362,13 +368,6 @@ export const sseHandlers: Record = { pendingPromise.finally(() => { context.pendingToolPromises.delete(toolCallId) }) - pendingPromise.catch((err) => { - logger.error('Parallel tool execution failed', { - toolCallId, - toolName, - error: err instanceof Error ? err.message : String(err), - }) - }) } if (options.interactive === false) { @@ -661,13 +660,6 @@ export const subAgentHandlers: Record = { pendingPromise.finally(() => { context.pendingToolPromises.delete(toolCallId) }) - pendingPromise.catch((err) => { - logger.error('Parallel subagent tool execution failed', { - toolCallId, - toolName, - error: err instanceof Error ? err.message : String(err), - }) - }) } if (options.interactive === false) { diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts index 0982ee796eb..edfdd110218 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts @@ -3,12 +3,7 @@ import { userTableRows } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { completeAsyncToolCall, markAsyncToolRunning } from '@/lib/copilot/async-runs/repository' -import { - TOOL_DECISION_INITIAL_POLL_MS, - TOOL_DECISION_MAX_POLL_MS, - TOOL_DECISION_POLL_BACKOFF, -} from '@/lib/copilot/constants' -import { getToolConfirmation } from '@/lib/copilot/orchestrator/persistence' +import { waitForToolConfirmation } from '@/lib/copilot/orchestrator/persistence' import { asRecord, markToolResultSeen, @@ -766,42 +761,14 @@ export async function executeToolAndReport( } } -function abortAwareSleep(ms: number, abortSignal?: AbortSignal): Promise { - return new Promise((resolve) => { - if (abortSignal?.aborted) { - resolve() - return - } - const timer = setTimeout(resolve, ms) - abortSignal?.addEventListener( - 'abort', - () => { - clearTimeout(timer) - resolve() - }, - { once: true } - ) - }) -} - export async function waitForToolDecision( toolCallId: string, timeoutMs: number, abortSignal?: AbortSignal ): Promise<{ status: string; message?: string } | null> { - const start = Date.now() - let interval = TOOL_DECISION_INITIAL_POLL_MS - const maxInterval = TOOL_DECISION_MAX_POLL_MS - while (Date.now() - start < timeoutMs) { - if (abortSignal?.aborted) return null - const decision = await getToolConfirmation(toolCallId) - if (decision?.status) { - return decision - } - await abortAwareSleep(interval, abortSignal) - interval = Math.min(interval * TOOL_DECISION_POLL_BACKOFF, maxInterval) - } - return null + const decision = await waitForToolConfirmation(toolCallId, timeoutMs, abortSignal) + if (!decision) return null + return { status: decision.status, message: decision.message } } /** @@ -814,31 +781,22 @@ export async function waitForToolDecision( * * Used for client-executable run tools: the client executes the workflow * and posts success/error to /api/copilot/confirm when done. The server - * polls here until that completion signal arrives. + * waits here until that completion signal arrives. */ export async function waitForToolCompletion( toolCallId: string, timeoutMs: number, abortSignal?: AbortSignal ): Promise<{ status: string; message?: string; data?: Record } | null> { - const start = Date.now() - let interval = TOOL_DECISION_INITIAL_POLL_MS - const maxInterval = TOOL_DECISION_MAX_POLL_MS - while (Date.now() - start < timeoutMs) { - if (abortSignal?.aborted) return null - const decision = await getToolConfirmation(toolCallId) - // Return on completion/terminal statuses, not intermediate 'accepted' - if ( - decision?.status === 'success' || - decision?.status === 'error' || - decision?.status === 'rejected' || - decision?.status === 'background' || - decision?.status === 'cancelled' - ) { - return decision - } - await abortAwareSleep(interval, abortSignal) - interval = Math.min(interval * TOOL_DECISION_POLL_BACKOFF, maxInterval) + const decision = await waitForToolConfirmation(toolCallId, timeoutMs, abortSignal) + if ( + decision?.status === 'success' || + decision?.status === 'error' || + decision?.status === 'rejected' || + decision?.status === 'background' || + decision?.status === 'cancelled' + ) { + return decision } return null } diff --git a/apps/sim/lib/copilot/orchestrator/stream/core.test.ts b/apps/sim/lib/copilot/orchestrator/stream/core.test.ts new file mode 100644 index 00000000000..97e614caf6b --- /dev/null +++ b/apps/sim/lib/copilot/orchestrator/stream/core.test.ts @@ -0,0 +1,63 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { buildToolCallSummaries } from '@/lib/copilot/orchestrator/stream/core' +import type { StreamingContext } from '@/lib/copilot/orchestrator/types' + +function makeContext(): StreamingContext { + return { + chatId: undefined, + requestId: undefined, + executionId: undefined, + runId: undefined, + messageId: 'msg-1', + accumulatedContent: '', + contentBlocks: [], + toolCalls: new Map(), + pendingToolPromises: new Map(), + awaitingAsyncContinuation: undefined, + currentThinkingBlock: null, + isInThinkingBlock: false, + subAgentParentToolCallId: undefined, + subAgentParentStack: [], + subAgentContent: {}, + subAgentToolCalls: {}, + pendingContent: '', + streamComplete: false, + wasAborted: false, + errors: [], + } +} + +describe('buildToolCallSummaries', () => { + it.concurrent('keeps pending tools as pending instead of defaulting to success', () => { + const context = makeContext() + context.toolCalls.set('tool-1', { + id: 'tool-1', + name: 'download_to_workspace_file', + status: 'pending', + startTime: 1, + }) + + const summaries = buildToolCallSummaries(context) + + expect(summaries).toHaveLength(1) + expect(summaries[0]?.status).toBe('pending') + }) + + it.concurrent('keeps executing tools as executing when no result exists yet', () => { + const context = makeContext() + context.toolCalls.set('tool-2', { + id: 'tool-2', + name: 'function_execute', + status: 'executing', + startTime: 1, + }) + + const summaries = buildToolCallSummaries(context) + + expect(summaries).toHaveLength(1) + expect(summaries[0]?.status).toBe('executing') + }) +}) diff --git a/apps/sim/lib/copilot/orchestrator/stream/core.ts b/apps/sim/lib/copilot/orchestrator/stream/core.ts index 7cfadf7a13f..96d85b61061 100644 --- a/apps/sim/lib/copilot/orchestrator/stream/core.ts +++ b/apps/sim/lib/copilot/orchestrator/stream/core.ts @@ -244,8 +244,8 @@ export function buildToolCallSummaries(context: StreamingContext): ToolCallSumma let status = toolCall.status if (toolCall.result && toolCall.result.success !== undefined) { status = toolCall.result.success ? 'success' : 'error' - } else if (status === 'pending' || status === 'executing') { - status = toolCall.error ? 'error' : 'success' + } else if ((status === 'pending' || status === 'executing') && toolCall.error) { + status = 'error' } return { id: toolCall.id, From 89342067dfee02a92cc618724707df01e0084321 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sun, 22 Mar 2026 21:49:35 -0700 Subject: [PATCH 17/31] Remove promptForToolApproval flag, fix workflow terminal logs --- apps/sim/app/api/copilot/chat/route.ts | 2 - apps/sim/app/api/mothership/chat/route.ts | 1 - .../orchestrator/sse/handlers/handlers.ts | 177 ++---------------- apps/sim/lib/copilot/orchestrator/types.ts | 7 - 4 files changed, 16 insertions(+), 171 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index f473b132960..296c28a8eb8 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -346,7 +346,6 @@ export async function POST(req: NextRequest) { goRoute: '/api/copilot', autoExecuteTools: true, interactive: true, - promptForToolApproval: false, onComplete: async (result: OrchestratorResult) => { if (!actualChatId) return @@ -426,7 +425,6 @@ export async function POST(req: NextRequest) { goRoute: '/api/copilot', autoExecuteTools: true, interactive: true, - promptForToolApproval: false, }) const responseData = { diff --git a/apps/sim/app/api/mothership/chat/route.ts b/apps/sim/app/api/mothership/chat/route.ts index 5822e81617a..a399820d645 100644 --- a/apps/sim/app/api/mothership/chat/route.ts +++ b/apps/sim/app/api/mothership/chat/route.ts @@ -280,7 +280,6 @@ export async function POST(req: NextRequest) { goRoute: '/api/mothership', autoExecuteTools: true, interactive: true, - promptForToolApproval: false, onComplete: async (result: OrchestratorResult) => { if (!actualChatId) return diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts index 20caba84e01..c2ded820d98 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts @@ -11,6 +11,7 @@ import { isToolAvailableOnSimSide, markToolComplete, } from '@/lib/copilot/orchestrator/tool-executor' +import { isWorkflowToolName } from '@/lib/copilot/workflow-tools' import type { ContentBlock, ExecutionContext, @@ -19,7 +20,7 @@ import type { StreamingContext, ToolCallState, } from '@/lib/copilot/orchestrator/types' -import { executeToolAndReport, waitForToolCompletion, waitForToolDecision } from './tool-execution' +import { executeToolAndReport, waitForToolCompletion } from './tool-execution' const logger = createLogger('CopilotSseHandlers') @@ -330,7 +331,7 @@ export const sseHandlers: Record = { const toolCall = context.toolCalls.get(toolCallId) if (!toolCall) return - const { requiresConfirmation, clientExecutable, internal } = getEventUI(event) + const { clientExecutable, internal } = getEventUI(event) if (internal) { return @@ -340,9 +341,11 @@ export const sseHandlers: Record = { return } - // Fire tool execution without awaiting so parallel tool calls from the - // same LLM turn execute concurrently. executeToolAndReport is self-contained: - // it updates tool state, calls markToolComplete, and emits result events. + /** + * Fire tool execution without awaiting so parallel tool calls from the + * same LLM turn execute concurrently. executeToolAndReport is self-contained: + * it updates tool state, calls markToolComplete, and emits result events. + */ const fireToolExecution = () => { void upsertAsyncToolCall({ runId: context.runId || crypto.randomUUID(), @@ -379,88 +382,14 @@ export const sseHandlers: Record = { return } - if (requiresConfirmation && options.promptForToolApproval) { - const decision = await waitForToolDecision( - toolCallId, - options.timeout || STREAM_TIMEOUT_MS, - options.abortSignal - ) - - if (decision?.status === 'accepted' || decision?.status === 'success') { - if (clientExecutable) { - toolCall.status = 'executing' - const completion = await waitForToolCompletion( - toolCallId, - options.timeout || STREAM_TIMEOUT_MS, - options.abortSignal - ) - handleClientCompletion(toolCall, toolCallId, completion) - await emitSyntheticToolResult(toolCallId, toolCall.name, completion, options) - return - } - if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) { - fireToolExecution() - } - return - } - - if (decision?.status === 'rejected' || decision?.status === 'error') { - toolCall.status = 'rejected' - toolCall.endTime = Date.now() - markToolComplete( - toolCall.id, - toolCall.name, - 400, - decision.message || 'Tool execution rejected', - { skipped: true, reason: 'user_rejected' } - ).catch((err) => { - logger.error('markToolComplete fire-and-forget failed (rejected)', { - toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), - }) - }) - markToolResultSeen(toolCall.id) - return - } - - if (decision?.status === 'background') { - toolCall.status = 'skipped' - toolCall.endTime = Date.now() - markToolComplete( - toolCall.id, - toolCall.name, - 202, - decision.message || 'Tool execution moved to background', - { background: true } - ).catch((err) => { - logger.error('markToolComplete fire-and-forget failed (background)', { - toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), - }) - }) - markToolResultSeen(toolCall.id) - return - } - - toolCall.status = 'rejected' - toolCall.endTime = Date.now() - markToolComplete(toolCall.id, toolCall.name, 408, 'Tool approval timed out', { - skipped: true, - reason: 'timeout', - }).catch((err) => { - logger.error('markToolComplete fire-and-forget failed (timeout)', { - toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), - }) - }) - markToolResultSeen(toolCall.id) - return - } - // Client-executable tool: execute server-side if available, otherwise // delegate to the client (React UI) and wait for completion. + // Workflow run tools are implemented on Sim for MCP/server callers but must + // still run in the browser when clientExecutable so the workflow terminal + // receives SSE block logs (executeWorkflowWithFullLogging). if (clientExecutable) { - if (isToolAvailableOnSimSide(toolName)) { + const delegateWorkflowRunToClient = isWorkflowToolName(toolName) + if (isToolAvailableOnSimSide(toolName) && !delegateWorkflowRunToClient) { if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) { fireToolExecution() } @@ -625,7 +554,7 @@ export const subAgentHandlers: Record = { if (isPartial) return - const { requiresConfirmation, clientExecutable, internal } = getEventUI(event) + const { clientExecutable, internal } = getEventUI(event) if (internal) { return @@ -671,83 +600,9 @@ export const subAgentHandlers: Record = { return } - if (requiresConfirmation && options.promptForToolApproval) { - const decision = await waitForToolDecision( - toolCallId, - options.timeout || STREAM_TIMEOUT_MS, - options.abortSignal - ) - if (decision?.status === 'accepted' || decision?.status === 'success') { - if (clientExecutable) { - toolCall.status = 'executing' - const completion = await waitForToolCompletion( - toolCallId, - options.timeout || STREAM_TIMEOUT_MS, - options.abortSignal - ) - handleClientCompletion(toolCall, toolCallId, completion) - await emitSyntheticToolResult(toolCallId, toolCall.name, completion, options) - return - } - if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) { - fireToolExecution() - } - return - } - if (decision?.status === 'rejected' || decision?.status === 'error') { - toolCall.status = 'rejected' - toolCall.endTime = Date.now() - markToolComplete( - toolCall.id, - toolCall.name, - 400, - decision.message || 'Tool execution rejected', - { skipped: true, reason: 'user_rejected' } - ).catch((err) => { - logger.error('markToolComplete fire-and-forget failed (subagent rejected)', { - toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), - }) - }) - markToolResultSeen(toolCall.id) - return - } - if (decision?.status === 'background') { - toolCall.status = 'skipped' - toolCall.endTime = Date.now() - markToolComplete( - toolCall.id, - toolCall.name, - 202, - decision.message || 'Tool execution moved to background', - { background: true } - ).catch((err) => { - logger.error('markToolComplete fire-and-forget failed (subagent background)', { - toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), - }) - }) - markToolResultSeen(toolCall.id) - return - } - - toolCall.status = 'rejected' - toolCall.endTime = Date.now() - markToolComplete(toolCall.id, toolCall.name, 408, 'Tool approval timed out', { - skipped: true, - reason: 'timeout', - }).catch((err) => { - logger.error('markToolComplete fire-and-forget failed (subagent timeout)', { - toolCallId: toolCall.id, - error: err instanceof Error ? err.message : String(err), - }) - }) - markToolResultSeen(toolCall.id) - return - } - if (clientExecutable) { - if (isToolAvailableOnSimSide(toolName)) { + const delegateWorkflowRunToClient = isWorkflowToolName(toolName) + if (isToolAvailableOnSimSide(toolName) && !delegateWorkflowRunToClient) { if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) { fireToolExecution() } diff --git a/apps/sim/lib/copilot/orchestrator/types.ts b/apps/sim/lib/copilot/orchestrator/types.ts index 92604cb6b72..c86169d499b 100644 --- a/apps/sim/lib/copilot/orchestrator/types.ts +++ b/apps/sim/lib/copilot/orchestrator/types.ts @@ -153,13 +153,6 @@ export interface OrchestratorOptions { onError?: (error: Error) => void | Promise abortSignal?: AbortSignal interactive?: boolean - /** - * When true, tools with `requiresConfirmation` will block until the client - * explicitly approves or rejects. When false (e.g. Mothership chat), those - * tools are auto-executed without waiting for user approval. - * Defaults to false. - */ - promptForToolApproval?: boolean } export interface OrchestratorResult { From 24fbf41a43446fc9c0c8eeffba0e87936bd029a0 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 12:11:59 -0700 Subject: [PATCH 18/31] Fixes --- apps/sim/lib/copilot/chat-streaming.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/sim/lib/copilot/chat-streaming.ts b/apps/sim/lib/copilot/chat-streaming.ts index 20d787eed58..eeef50be0c8 100644 --- a/apps/sim/lib/copilot/chat-streaming.ts +++ b/apps/sim/lib/copilot/chat-streaming.ts @@ -276,11 +276,9 @@ export function createSSEStream(params: StreamingOrchestrationParams): ReadableS 'An unexpected error occurred while processing the response.' if (clientDisconnected) { - logger.info(`[${requestId}] Stream ended after client disconnect`) - await eventWriter.close().catch(() => {}) - await setStreamMeta(streamId, { status: 'cancelled', userId, executionId, runId }) - await updateRunStatus(runId, 'cancelled', { completedAt: new Date() }).catch(() => {}) - return + logger.info(`[${requestId}] Stream failed after client disconnect`, { + error: errorMessage, + }) } logger.error(`[${requestId}] Orchestration returned failure`, { @@ -313,11 +311,9 @@ export function createSSEStream(params: StreamingOrchestrationParams): ReadableS return } if (clientDisconnected) { - logger.info(`[${requestId}] Stream ended after client disconnect`) - await eventWriter.close().catch(() => {}) - await setStreamMeta(streamId, { status: 'cancelled', userId, executionId, runId }) - await updateRunStatus(runId, 'cancelled', { completedAt: new Date() }).catch(() => {}) - return + logger.info(`[${requestId}] Stream errored after client disconnect`, { + error: error instanceof Error ? error.message : 'Stream error', + }) } logger.error(`[${requestId}] Orchestration error:`, error) await eventWriter.close() From 7d5976308fa565fefd82e7313fdee644dcde65c3 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 12:44:16 -0700 Subject: [PATCH 19/31] Fix buffer --- apps/sim/app/api/copilot/chat/stream/route.ts | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/stream/route.ts b/apps/sim/app/api/copilot/chat/stream/route.ts index 35b66fa3eca..485508114f5 100644 --- a/apps/sim/app/api/copilot/chat/stream/route.ts +++ b/apps/sim/app/api/copilot/chat/stream/route.ts @@ -80,6 +80,33 @@ export async function GET(request: NextRequest) { async start(controller) { let lastEventId = Number.isFinite(fromEventId) ? fromEventId : 0 let latestMeta = meta + let controllerClosed = false + + const closeController = () => { + if (controllerClosed) return + controllerClosed = true + try { + controller.close() + } catch { + // Controller already closed by runtime/client - treat as normal. + } + } + + const enqueueEvent = (payload: Record) => { + if (controllerClosed) return false + try { + controller.enqueue(encodeEvent(payload)) + return true + } catch { + controllerClosed = true + return false + } + } + + const abortListener = () => { + controllerClosed = true + } + request.signal.addEventListener('abort', abortListener, { once: true }) const flushEvents = async () => { const events = await readStreamEvents(streamId, lastEventId) @@ -99,37 +126,46 @@ export async function GET(request: NextRequest) { executionId: latestMeta?.executionId, runId: latestMeta?.runId, } - controller.enqueue(encodeEvent(payload)) + if (!enqueueEvent(payload)) { + break + } } } try { await flushEvents() - while (Date.now() - startTime < MAX_STREAM_MS) { + while (!controllerClosed && Date.now() - startTime < MAX_STREAM_MS) { const currentMeta = await getStreamMeta(streamId) if (!currentMeta) break latestMeta = currentMeta await flushEvents() + if (controllerClosed) { + break + } if (currentMeta.status === 'complete' || currentMeta.status === 'error') { break } if (request.signal.aborted) { + controllerClosed = true break } await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) } } catch (error) { - logger.warn('Stream replay failed', { - streamId, - error: error instanceof Error ? error.message : String(error), - }) + if (!controllerClosed && !request.signal.aborted) { + logger.warn('Stream replay failed', { + streamId, + error: error instanceof Error ? error.message : String(error), + }) + } } finally { - controller.close() + request.signal.removeEventListener('abort', abortListener) + closeController() } }, }) From 035c614ab69c9dc2db7ea8d6e375feffe646bed8 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 12:51:03 -0700 Subject: [PATCH 20/31] Fix --- .../orchestrator/sse/handlers/handlers.ts | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts index 6bcbe0a2704..e4bbba660c3 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts @@ -347,26 +347,26 @@ export const sseHandlers: Record = { * it updates tool state, calls markToolComplete, and emits result events. */ const fireToolExecution = () => { - void upsertAsyncToolCall({ - runId: context.runId || crypto.randomUUID(), - toolCallId, - toolName, - args, - }).catch(() => {}) - const pendingPromise = executeToolAndReport(toolCallId, context, execContext, options).catch( - (err) => { - logger.error('Parallel tool execution failed', { - toolCallId, - toolName, - error: err instanceof Error ? err.message : String(err), - }) - return { - status: 'error', - message: err instanceof Error ? err.message : String(err), - data: { error: err instanceof Error ? err.message : String(err) }, - } + const pendingPromise = (async () => { + await upsertAsyncToolCall({ + runId: context.runId || crypto.randomUUID(), + toolCallId, + toolName, + args, + }) + return executeToolAndReport(toolCallId, context, execContext, options) + })().catch((err) => { + logger.error('Parallel tool execution failed', { + toolCallId, + toolName, + error: err instanceof Error ? err.message : String(err), + }) + return { + status: 'error', + message: err instanceof Error ? err.message : String(err), + data: { error: err instanceof Error ? err.message : String(err) }, } - ) + }) context.pendingToolPromises.set(toolCallId, pendingPromise) pendingPromise.finally(() => { context.pendingToolPromises.delete(toolCallId) @@ -565,26 +565,26 @@ export const subAgentHandlers: Record = { } const fireToolExecution = () => { - void upsertAsyncToolCall({ - runId: context.runId || crypto.randomUUID(), - toolCallId, - toolName, - args, - }).catch(() => {}) - const pendingPromise = executeToolAndReport(toolCallId, context, execContext, options).catch( - (err) => { - logger.error('Parallel subagent tool execution failed', { - toolCallId, - toolName, - error: err instanceof Error ? err.message : String(err), - }) - return { - status: 'error', - message: err instanceof Error ? err.message : String(err), - data: { error: err instanceof Error ? err.message : String(err) }, - } + const pendingPromise = (async () => { + await upsertAsyncToolCall({ + runId: context.runId || crypto.randomUUID(), + toolCallId, + toolName, + args, + }) + return executeToolAndReport(toolCallId, context, execContext, options) + })().catch((err) => { + logger.error('Parallel subagent tool execution failed', { + toolCallId, + toolName, + error: err instanceof Error ? err.message : String(err), + }) + return { + status: 'error', + message: err instanceof Error ? err.message : String(err), + data: { error: err instanceof Error ? err.message : String(err) }, } - ) + }) context.pendingToolPromises.set(toolCallId, pendingPromise) pendingPromise.finally(() => { context.pendingToolPromises.delete(toolCallId) From 7dce59e066fce593ff6208a7303f305c12f99ec0 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 12:55:01 -0700 Subject: [PATCH 21/31] Fix claimed by --- apps/sim/lib/copilot/async-runs/repository.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/sim/lib/copilot/async-runs/repository.ts b/apps/sim/lib/copilot/async-runs/repository.ts index 20fb91ba936..9ac169f7765 100644 --- a/apps/sim/lib/copilot/async-runs/repository.ts +++ b/apps/sim/lib/copilot/async-runs/repository.ts @@ -169,17 +169,18 @@ export async function markAsyncToolStatus( status: CopilotAsyncToolStatus, updates: { claimedBy?: string | null + claimedAt?: Date | null result?: Record | null error?: string | null completedAt?: Date | null } = {} ) { const claimedAt = - status === 'running' && updates.claimedBy - ? updates.completedAt - ? undefined - : new Date() - : undefined + updates.claimedAt !== undefined + ? updates.claimedAt + : status === 'running' && updates.claimedBy + ? new Date() + : undefined const [row] = await db .update(copilotAsyncToolCalls) @@ -223,6 +224,8 @@ export async function completeAsyncToolCall(input: { } return markAsyncToolStatus(input.toolCallId, input.status, { + claimedBy: null, + claimedAt: null, result: input.result ?? null, error: input.error ?? null, completedAt: new Date(), From f77c8b4c7d80e96cfeba41789b6a6ef3d9077094 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 13:17:30 -0700 Subject: [PATCH 22/31] Cleanup v1 --- .../app/api/copilot/chat/stream/route.test.ts | 59 +++ apps/sim/app/api/copilot/chat/stream/route.ts | 6 +- .../sim/app/api/copilot/confirm/route.test.ts | 451 ++++++------------ apps/sim/app/api/copilot/confirm/route.ts | 45 +- .../[workspaceId]/home/hooks/use-chat.ts | 24 +- apps/sim/lib/copilot/async-runs/repository.ts | 12 +- apps/sim/lib/copilot/chat-streaming.test.ts | 140 ++++++ apps/sim/lib/copilot/chat-streaming.ts | 25 +- .../lib/copilot/orchestrator/index.test.ts | 141 ++++++ apps/sim/lib/copilot/orchestrator/index.ts | 61 ++- .../sse/handlers/handlers.test.ts | 37 ++ .../orchestrator/sse/handlers/handlers.ts | 40 +- 12 files changed, 677 insertions(+), 364 deletions(-) create mode 100644 apps/sim/app/api/copilot/chat/stream/route.test.ts create mode 100644 apps/sim/lib/copilot/chat-streaming.test.ts create mode 100644 apps/sim/lib/copilot/orchestrator/index.test.ts diff --git a/apps/sim/app/api/copilot/chat/stream/route.test.ts b/apps/sim/app/api/copilot/chat/stream/route.test.ts new file mode 100644 index 00000000000..993f10501a8 --- /dev/null +++ b/apps/sim/app/api/copilot/chat/stream/route.test.ts @@ -0,0 +1,59 @@ +/** + * @vitest-environment node + */ + +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { getStreamMeta, readStreamEvents, authenticateCopilotRequestSessionOnly } = vi.hoisted( + () => ({ + getStreamMeta: vi.fn(), + readStreamEvents: vi.fn(), + authenticateCopilotRequestSessionOnly: vi.fn(), + }) +) + +vi.mock('@/lib/copilot/orchestrator/stream/buffer', () => ({ + getStreamMeta, + readStreamEvents, +})) + +vi.mock('@/lib/copilot/request-helpers', () => ({ + authenticateCopilotRequestSessionOnly, +})) + +import { GET } from '@/app/api/copilot/chat/stream/route' + +describe('copilot chat stream replay route', () => { + beforeEach(() => { + vi.clearAllMocks() + authenticateCopilotRequestSessionOnly.mockResolvedValue({ + userId: 'user-1', + isAuthenticated: true, + }) + readStreamEvents.mockResolvedValue([]) + }) + + it('stops replay polling when stream meta becomes cancelled', async () => { + getStreamMeta + .mockResolvedValueOnce({ + status: 'active', + userId: 'user-1', + }) + .mockResolvedValueOnce({ + status: 'cancelled', + userId: 'user-1', + }) + + const response = await GET( + new NextRequest('http://localhost:3000/api/copilot/chat/stream?streamId=stream-1') + ) + + const reader = response.body?.getReader() + expect(reader).toBeTruthy() + + const first = await reader!.read() + expect(first.done).toBe(true) + expect(getStreamMeta).toHaveBeenCalledTimes(2) + }) +}) diff --git a/apps/sim/app/api/copilot/chat/stream/route.ts b/apps/sim/app/api/copilot/chat/stream/route.ts index 485508114f5..3234b98d085 100644 --- a/apps/sim/app/api/copilot/chat/stream/route.ts +++ b/apps/sim/app/api/copilot/chat/stream/route.ts @@ -145,7 +145,11 @@ export async function GET(request: NextRequest) { if (controllerClosed) { break } - if (currentMeta.status === 'complete' || currentMeta.status === 'error') { + if ( + currentMeta.status === 'complete' || + currentMeta.status === 'error' || + currentMeta.status === 'cancelled' + ) { break } diff --git a/apps/sim/app/api/copilot/confirm/route.test.ts b/apps/sim/app/api/copilot/confirm/route.test.ts index e5bd3c9da42..ee501f6739a 100644 --- a/apps/sim/app/api/copilot/confirm/route.test.ts +++ b/apps/sim/app/api/copilot/confirm/route.test.ts @@ -1,59 +1,95 @@ /** - * Tests for copilot confirm API route - * * @vitest-environment node */ import { NextRequest } from 'next/server' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + authenticateCopilotRequestSessionOnly, + createBadRequestResponse, + createInternalServerErrorResponse, + createNotFoundResponse, + createRequestTracker, + createUnauthorizedResponse, + getAsyncToolCall, + getRunSegment, + upsertAsyncToolCall, + completeAsyncToolCall, + getRedisClient, + publishToolConfirmation, +} = vi.hoisted(() => ({ + authenticateCopilotRequestSessionOnly: vi.fn(), + createBadRequestResponse: vi.fn((message: string) => + Response.json({ error: message }, { status: 400 }) + ), + createInternalServerErrorResponse: vi.fn((message: string) => + Response.json({ error: message }, { status: 500 }) + ), + createNotFoundResponse: vi.fn((message: string) => + Response.json({ error: message }, { status: 404 }) + ), + createRequestTracker: vi.fn(() => ({ requestId: 'req-1', getDuration: () => 1 })), + createUnauthorizedResponse: vi.fn(() => + Response.json({ error: 'Unauthorized' }, { status: 401 }) + ), + getAsyncToolCall: vi.fn(), + getRunSegment: vi.fn(), + upsertAsyncToolCall: vi.fn(), + completeAsyncToolCall: vi.fn(), + getRedisClient: vi.fn(), + publishToolConfirmation: vi.fn(), +})) -const { mockGetSession, mockRedisExists, mockRedisSet, mockGetRedisClient } = vi.hoisted(() => ({ - mockGetSession: vi.fn(), - mockRedisExists: vi.fn(), - mockRedisSet: vi.fn(), - mockGetRedisClient: vi.fn(), +vi.mock('@/lib/copilot/request-helpers', () => ({ + authenticateCopilotRequestSessionOnly, + createBadRequestResponse, + createInternalServerErrorResponse, + createNotFoundResponse, + createRequestTracker, + createUnauthorizedResponse, })) -vi.mock('@/lib/auth', () => ({ - getSession: mockGetSession, +vi.mock('@/lib/copilot/async-runs/repository', () => ({ + getAsyncToolCall, + getRunSegment, + upsertAsyncToolCall, + completeAsyncToolCall, })) vi.mock('@/lib/core/config/redis', () => ({ - getRedisClient: mockGetRedisClient, + getRedisClient, +})) + +vi.mock('@/lib/copilot/orchestrator/persistence', () => ({ + publishToolConfirmation, })) -import { POST } from '@/app/api/copilot/confirm/route' +import { POST } from './route' describe('Copilot Confirm API Route', () => { + const existingRow = { + toolCallId: 'tool-call-123', + runId: 'run-1', + checkpointId: 'checkpoint-1', + toolName: 'client_tool', + args: { foo: 'bar' }, + } + beforeEach(() => { vi.clearAllMocks() - - const mockRedisClient = { - exists: mockRedisExists, - set: mockRedisSet, - } - - mockGetRedisClient.mockReturnValue(mockRedisClient) - mockRedisExists.mockResolvedValue(1) - mockRedisSet.mockResolvedValue('OK') - - vi.spyOn(global, 'setTimeout').mockImplementation((callback, _delay) => { - if (typeof callback === 'function') { - setImmediate(callback) - } - return setTimeout(() => {}, 0) as unknown as NodeJS.Timeout + authenticateCopilotRequestSessionOnly.mockResolvedValue({ + userId: 'user-1', + isAuthenticated: true, }) - - let mockTime = 1640995200000 - vi.spyOn(Date, 'now').mockImplementation(() => { - mockTime += 10000 - return mockTime + getAsyncToolCall.mockResolvedValue(existingRow) + getRunSegment.mockResolvedValue({ id: 'run-1', userId: 'user-1' }) + upsertAsyncToolCall.mockResolvedValue(existingRow) + completeAsyncToolCall.mockResolvedValue(existingRow) + getRedisClient.mockReturnValue({ + set: vi.fn().mockResolvedValue('OK'), }) }) - afterEach(() => { - vi.restoreAllMocks() - }) - function createMockPostRequest(body: Record): NextRequest { return new NextRequest('http://localhost:3000/api/copilot/confirm', { method: 'POST', @@ -62,311 +98,118 @@ describe('Copilot Confirm API Route', () => { }) } - function setAuthenticated() { - mockGetSession.mockResolvedValue({ - user: { id: 'test-user-id', email: 'test@example.com', name: 'Test User' }, + it('returns 401 when the session is unauthenticated', async () => { + authenticateCopilotRequestSessionOnly.mockResolvedValue({ + userId: null, + isAuthenticated: false, }) - } - - function setUnauthenticated() { - mockGetSession.mockResolvedValue(null) - } - - describe('POST', () => { - it('should return 401 when user is not authenticated', async () => { - setUnauthenticated() - const req = createMockPostRequest({ + const response = await POST( + createMockPostRequest({ toolCallId: 'tool-call-123', status: 'success', }) + ) - const response = await POST(req) - - expect(response.status).toBe(401) - const responseData = await response.json() - expect(responseData).toEqual({ error: 'Unauthorized' }) - }) + expect(response.status).toBe(401) + expect(await response.json()).toEqual({ error: 'Unauthorized' }) + }) - it('should return 400 for invalid request body - missing toolCallId', async () => { - setAuthenticated() + it('returns 404 when the tool call row does not exist', async () => { + getAsyncToolCall.mockResolvedValue(null) - const req = createMockPostRequest({ + const response = await POST( + createMockPostRequest({ + toolCallId: 'missing-tool', status: 'success', }) + ) - const response = await POST(req) - - expect(response.status).toBe(400) - const responseData = await response.json() - expect(responseData.error).toContain('Required') - }) - - it('should return 400 for invalid request body - missing status', async () => { - setAuthenticated() - - const req = createMockPostRequest({ - toolCallId: 'tool-call-123', - }) - - const response = await POST(req) - - expect(response.status).toBe(400) - const responseData = await response.json() - expect(responseData.error).toContain('Invalid request data') - }) + expect(response.status).toBe(404) + expect(await response.json()).toEqual({ error: 'Tool call not found' }) + }) - it('should return 400 for invalid status value', async () => { - setAuthenticated() + it('returns 403 when the tool call belongs to a different user', async () => { + getRunSegment.mockResolvedValue({ id: 'run-1', userId: 'user-2' }) - const req = createMockPostRequest({ + const response = await POST( + createMockPostRequest({ toolCallId: 'tool-call-123', - status: 'invalid-status', + status: 'success', }) + ) - const response = await POST(req) - - expect(response.status).toBe(400) - const responseData = await response.json() - expect(responseData.error).toContain('Invalid notification status') - }) + expect(response.status).toBe(403) + expect(await response.json()).toEqual({ error: 'Forbidden' }) + }) - it('should successfully confirm tool call with success status', async () => { - setAuthenticated() + it('persists terminal confirmations through completeAsyncToolCall', async () => { + const redisSet = vi.fn().mockResolvedValue('OK') + getRedisClient.mockReturnValue({ set: redisSet }) - const req = createMockPostRequest({ + const response = await POST( + createMockPostRequest({ toolCallId: 'tool-call-123', status: 'success', message: 'Tool executed successfully', + data: { ok: true }, }) + ) - const response = await POST(req) - - expect(response.status).toBe(200) - const responseData = await response.json() - expect(responseData).toEqual({ - success: true, - message: 'Tool executed successfully', - toolCallId: 'tool-call-123', - status: 'success', - }) - - expect(mockRedisSet).toHaveBeenCalled() - }) - - it('should successfully confirm tool call with error status', async () => { - setAuthenticated() - - const req = createMockPostRequest({ - toolCallId: 'tool-call-456', - status: 'error', - message: 'Tool execution failed', - }) - - const response = await POST(req) - - expect(response.status).toBe(200) - const responseData = await response.json() - expect(responseData).toEqual({ - success: true, - message: 'Tool execution failed', - toolCallId: 'tool-call-456', - status: 'error', - }) - - expect(mockRedisSet).toHaveBeenCalled() - }) - - it('should successfully confirm tool call with accepted status', async () => { - setAuthenticated() - - const req = createMockPostRequest({ - toolCallId: 'tool-call-789', - status: 'accepted', - }) - - const response = await POST(req) - - expect(response.status).toBe(200) - const responseData = await response.json() - expect(responseData).toEqual({ - success: true, - message: 'Tool call tool-call-789 has been accepted', - toolCallId: 'tool-call-789', - status: 'accepted', - }) - - expect(mockRedisSet).toHaveBeenCalled() - }) - - it('should successfully confirm tool call with rejected status', async () => { - setAuthenticated() - - const req = createMockPostRequest({ - toolCallId: 'tool-call-101', - status: 'rejected', - }) - - const response = await POST(req) - - expect(response.status).toBe(200) - const responseData = await response.json() - expect(responseData).toEqual({ - success: true, - message: 'Tool call tool-call-101 has been rejected', - toolCallId: 'tool-call-101', - status: 'rejected', - }) + expect(response.status).toBe(200) + expect(completeAsyncToolCall).toHaveBeenCalledWith({ + toolCallId: 'tool-call-123', + status: 'completed', + result: { ok: true }, + error: null, }) - - it('should successfully confirm tool call with background status', async () => { - setAuthenticated() - - const req = createMockPostRequest({ - toolCallId: 'tool-call-bg', - status: 'background', - message: 'Moved to background execution', - }) - - const response = await POST(req) - - expect(response.status).toBe(200) - const responseData = await response.json() - expect(responseData).toEqual({ - success: true, - message: 'Moved to background execution', - toolCallId: 'tool-call-bg', - status: 'background', - }) - }) - - it('should succeed when Redis client is not available', async () => { - setAuthenticated() - - mockGetRedisClient.mockReturnValue(null) - - const req = createMockPostRequest({ + expect(upsertAsyncToolCall).not.toHaveBeenCalled() + expect(redisSet).toHaveBeenCalledOnce() + expect(publishToolConfirmation).toHaveBeenCalledWith( + expect.objectContaining({ toolCallId: 'tool-call-123', status: 'success', + data: { ok: true }, }) + ) + }) - const response = await POST(req) - - expect(response.status).toBe(200) - const responseData = await response.json() - expect(responseData).toEqual({ - success: true, - message: 'Tool call tool-call-123 has been success', - toolCallId: 'tool-call-123', - status: 'success', - }) - expect(mockRedisSet).not.toHaveBeenCalled() - }) - - it('should return 400 when Redis set fails', async () => { - setAuthenticated() - - mockRedisSet.mockRejectedValueOnce(new Error('Redis set failed')) - - const req = createMockPostRequest({ - toolCallId: 'non-existent-tool', - status: 'success', - }) - - const response = await POST(req) - - expect(response.status).toBe(400) - const responseData = await response.json() - expect(responseData.error).toBe('Failed to update tool call status or tool call not found') - }, 10000) - - it('should handle Redis errors gracefully', async () => { - setAuthenticated() - - mockRedisSet.mockRejectedValueOnce(new Error('Redis connection failed')) - - const req = createMockPostRequest({ + it('uses upsertAsyncToolCall for non-terminal confirmations', async () => { + const response = await POST( + createMockPostRequest({ toolCallId: 'tool-call-123', - status: 'success', + status: 'accepted', }) + ) - const response = await POST(req) - - expect(response.status).toBe(400) - const responseData = await response.json() - expect(responseData.error).toBe('Failed to update tool call status or tool call not found') + expect(response.status).toBe(200) + expect(upsertAsyncToolCall).toHaveBeenCalledWith({ + runId: 'run-1', + checkpointId: 'checkpoint-1', + toolCallId: 'tool-call-123', + toolName: 'client_tool', + args: { foo: 'bar' }, + status: 'pending', }) + expect(completeAsyncToolCall).not.toHaveBeenCalled() + }) - it('should handle Redis set operation failure', async () => { - setAuthenticated() - - mockRedisExists.mockResolvedValue(1) - mockRedisSet.mockRejectedValue(new Error('Redis set failed')) + it('publishes confirmation even when Redis is unavailable', async () => { + getRedisClient.mockReturnValue(null) - const req = createMockPostRequest({ + const response = await POST( + createMockPostRequest({ toolCallId: 'tool-call-123', - status: 'success', - }) - - const response = await POST(req) - - expect(response.status).toBe(400) - const responseData = await response.json() - expect(responseData.error).toBe('Failed to update tool call status or tool call not found') - }) - - it('should handle JSON parsing errors in request body', async () => { - setAuthenticated() - - const req = new NextRequest('http://localhost:3000/api/copilot/confirm', { - method: 'POST', - body: '{invalid-json', - headers: { - 'Content-Type': 'application/json', - }, + status: 'background', }) + ) - const response = await POST(req) - - expect(response.status).toBe(500) - const responseData = await response.json() - expect(responseData.error).toContain('JSON') - }) - - it('should validate empty toolCallId', async () => { - setAuthenticated() - - const req = createMockPostRequest({ - toolCallId: '', - status: 'success', + expect(response.status).toBe(200) + expect(publishToolConfirmation).toHaveBeenCalledWith( + expect.objectContaining({ + toolCallId: 'tool-call-123', + status: 'background', }) - - const response = await POST(req) - - expect(response.status).toBe(400) - const responseData = await response.json() - expect(responseData.error).toContain('Tool call ID is required') - }) - - it('should handle all valid status types', async () => { - setAuthenticated() - - const validStatuses = ['success', 'error', 'accepted', 'rejected', 'background'] - - for (const status of validStatuses) { - const req = createMockPostRequest({ - toolCallId: `tool-call-${status}`, - status, - }) - - const response = await POST(req) - - expect(response.status).toBe(200) - const responseData = await response.json() - expect(responseData.success).toBe(true) - expect(responseData.status).toBe(status) - expect(responseData.toolCallId).toBe(`tool-call-${status}`) - } - }) + ) }) }) diff --git a/apps/sim/app/api/copilot/confirm/route.ts b/apps/sim/app/api/copilot/confirm/route.ts index 20844491ef8..a49c628f9ed 100644 --- a/apps/sim/app/api/copilot/confirm/route.ts +++ b/apps/sim/app/api/copilot/confirm/route.ts @@ -4,6 +4,7 @@ import { z } from 'zod' import { completeAsyncToolCall, getAsyncToolCall, + getRunSegment, upsertAsyncToolCall, } from '@/lib/copilot/async-runs/repository' import { REDIS_TOOL_CALL_PREFIX, REDIS_TOOL_CALL_TTL_SECONDS } from '@/lib/copilot/constants' @@ -12,6 +13,7 @@ import { authenticateCopilotRequestSessionOnly, createBadRequestResponse, createInternalServerErrorResponse, + createNotFoundResponse, createRequestTracker, createUnauthorizedResponse, type NotificationStatus, @@ -35,11 +37,12 @@ const ConfirmationSchema = z.object({ * waitForToolDecision() polls Redis for this value. */ async function updateToolCallStatus( - toolCallId: string, + existing: NonNullable>>, status: NotificationStatus, message?: string, data?: Record ): Promise { + const toolCallId = existing.toolCallId const durableStatus = status === 'success' ? 'completed' @@ -48,22 +51,6 @@ async function updateToolCallStatus( : status === 'error' || status === 'rejected' ? 'failed' : 'pending' - const existing = await getAsyncToolCall(toolCallId).catch(() => null) - if (existing?.runId) { - await upsertAsyncToolCall({ - runId: existing.runId, - checkpointId: existing.checkpointId ?? null, - toolCallId, - toolName: existing.toolName || 'client_tool', - args: (existing.args as Record | null) ?? {}, - status: durableStatus, - }).catch(() => {}) - } else { - logger.warn('Tool confirmation has no existing async tool row; durable state may be missing', { - toolCallId, - status, - }) - } if ( durableStatus === 'completed' || durableStatus === 'failed' || @@ -75,6 +62,15 @@ async function updateToolCallStatus( result: data ?? null, error: status === 'success' ? null : message || status, }).catch(() => {}) + } else if (existing.runId) { + await upsertAsyncToolCall({ + runId: existing.runId, + checkpointId: existing.checkpointId ?? null, + toolCallId, + toolName: existing.toolName || 'client_tool', + args: (existing.args as Record | null) ?? {}, + status: durableStatus, + }).catch(() => {}) } const redis = getRedisClient() @@ -137,9 +133,22 @@ export async function POST(req: NextRequest) { const body = await req.json() const { toolCallId, status, message, data } = ConfirmationSchema.parse(body) + const existing = await getAsyncToolCall(toolCallId).catch(() => null) + + if (!existing) { + return createNotFoundResponse('Tool call not found') + } + + const run = await getRunSegment(existing.runId).catch(() => null) + if (!run) { + return createNotFoundResponse('Tool call run not found') + } + if (run.userId !== authenticatedUserId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } // Update the tool call status in Redis - const updated = await updateToolCallStatus(toolCallId, status, message, data) + const updated = await updateToolCallStatus(existing, status, message, data) if (!updated) { logger.error(`[${tracker.requestId}] Failed to update tool call status`, { 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 706c104f6a3..7ee0d093bf9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -82,6 +82,8 @@ const STATE_TO_STATUS: Record = { } as const const DEPLOY_TOOL_NAMES = new Set(['deploy_api', 'deploy_chat', 'deploy_mcp', 'redeploy']) +const RECONNECT_TAIL_ERROR = + 'Live reconnect failed before the stream finished. The latest response may be incomplete.' function mapStoredBlock(block: TaskStoredContentBlock): ContentBlock { const mapped: ContentBlock = { @@ -528,7 +530,8 @@ export function useChat( const lastEventId = batchEvents.length > 0 ? batchEvents[batchEvents.length - 1].eventId : 0 - const isStreamDone = streamStatus === 'complete' || streamStatus === 'error' + const isStreamDone = + streamStatus === 'complete' || streamStatus === 'error' || streamStatus === 'cancelled' const combinedStream = new ReadableStream({ async start(controller) { @@ -545,7 +548,13 @@ export function useChat( `/api/copilot/chat/stream?streamId=${activeStreamId}&from=${lastEventId}`, { signal: abortController.signal } ) - if (sseRes.ok && sseRes.body) { + if (!sseRes.ok || !sseRes.body) { + logger.warn('SSE tail reconnect returned no readable body', { + status: sseRes.status, + streamId: activeStreamId, + }) + setError(RECONNECT_TAIL_ERROR) + } else { const reader = sseRes.body.getReader() while (true) { const { done, value } = await reader.read() @@ -556,6 +565,7 @@ export function useChat( } catch (err) { if (!(err instanceof Error && err.name === 'AbortError')) { logger.warn('SSE tail failed during reconnect', err) + setError(RECONNECT_TAIL_ERROR) } } } @@ -1063,6 +1073,11 @@ export function useChat( const idx = toolMap.get(id) if (idx !== undefined && blocks[idx].toolCall) { blocks[idx].toolCall!.status = 'error' + if (blocks[idx].toolCall?.name === 'workspace_file') { + setStreamingFile(null) + streamingFileRef.current = null + setResources((rs) => rs.filter((resource) => resource.id !== 'streaming-file')) + } flush() } break @@ -1084,11 +1099,6 @@ export function useChat( break } case 'subagent_end': { - if (activeSubagent === 'file_write') { - setStreamingFile(null) - streamingFileRef.current = null - setResources((rs) => rs.filter((r) => r.id !== 'streaming-file')) - } activeSubagent = undefined blocks.push({ type: 'subagent_end' }) flush() diff --git a/apps/sim/lib/copilot/async-runs/repository.ts b/apps/sim/lib/copilot/async-runs/repository.ts index 9ac169f7765..4f636be4f68 100644 --- a/apps/sim/lib/copilot/async-runs/repository.ts +++ b/apps/sim/lib/copilot/async-runs/repository.ts @@ -86,6 +86,11 @@ export async function getLatestRunForExecution(executionId: string) { return run ?? null } +export async function getRunSegment(runId: string) { + const [run] = await db.select().from(copilotRuns).where(eq(copilotRuns.id, runId)).limit(1) + return run ?? null +} + export async function createRunCheckpoint(input: { runId: string pendingToolCallId: string @@ -219,7 +224,11 @@ export async function completeAsyncToolCall(input: { return null } - if (isTerminalAsyncStatus(existing.status)) { + if ( + isTerminalAsyncStatus(existing.status) || + existing.status === 'resume_enqueued' || + existing.status === 'resumed' + ) { return existing } @@ -260,7 +269,6 @@ export async function claimCompletedAsyncToolCall(toolCallId: string, workerId: const [row] = await db .update(copilotAsyncToolCalls) .set({ - status: 'resume_enqueued', claimedBy: workerId, claimedAt: new Date(), updatedAt: new Date(), diff --git a/apps/sim/lib/copilot/chat-streaming.test.ts b/apps/sim/lib/copilot/chat-streaming.test.ts new file mode 100644 index 00000000000..b0e05ed445a --- /dev/null +++ b/apps/sim/lib/copilot/chat-streaming.test.ts @@ -0,0 +1,140 @@ +/** + * @vitest-environment node + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + orchestrateCopilotStream, + createRunSegment, + updateRunStatus, + resetStreamBuffer, + setStreamMeta, + createStreamEventWriter, +} = vi.hoisted(() => ({ + orchestrateCopilotStream: vi.fn(), + createRunSegment: vi.fn(), + updateRunStatus: vi.fn(), + resetStreamBuffer: vi.fn(), + setStreamMeta: vi.fn(), + createStreamEventWriter: vi.fn(), +})) + +vi.mock('@/lib/copilot/orchestrator', () => ({ + orchestrateCopilotStream, +})) + +vi.mock('@/lib/copilot/async-runs/repository', () => ({ + createRunSegment, + updateRunStatus, +})) + +vi.mock('@/lib/copilot/orchestrator/stream/buffer', () => ({ + createStreamEventWriter, + resetStreamBuffer, + setStreamMeta, +})) + +vi.mock('@sim/db', () => ({ + db: { + update: vi.fn(() => ({ + set: vi.fn(() => ({ + where: vi.fn(), + })), + })), + }, +})) + +vi.mock('@/lib/copilot/task-events', () => ({ + taskPubSub: null, +})) + +import { createSSEStream } from '@/lib/copilot/chat-streaming' + +async function drainStream(stream: ReadableStream) { + const reader = stream.getReader() + while (true) { + const { done } = await reader.read() + if (done) break + } +} + +describe('createSSEStream terminal error handling', () => { + const write = vi.fn().mockResolvedValue({ eventId: 1, streamId: 'stream-1', event: {} }) + const flush = vi.fn().mockResolvedValue(undefined) + const close = vi.fn().mockResolvedValue(undefined) + + beforeEach(() => { + vi.clearAllMocks() + write.mockResolvedValue({ eventId: 1, streamId: 'stream-1', event: {} }) + flush.mockResolvedValue(undefined) + close.mockResolvedValue(undefined) + createStreamEventWriter.mockReturnValue({ write, flush, close }) + resetStreamBuffer.mockResolvedValue(undefined) + setStreamMeta.mockResolvedValue(undefined) + createRunSegment.mockResolvedValue(null) + updateRunStatus.mockResolvedValue(null) + }) + + it('writes a terminal error event before close when orchestration returns success=false', async () => { + orchestrateCopilotStream.mockResolvedValue({ + success: false, + error: 'resume failed', + content: '', + contentBlocks: [], + toolCalls: [], + }) + + const stream = createSSEStream({ + requestPayload: { message: 'hello' }, + userId: 'user-1', + streamId: 'stream-1', + executionId: 'exec-1', + runId: 'run-1', + currentChat: null, + isNewChat: false, + message: 'hello', + titleModel: 'gpt-5.4', + requestId: 'req-1', + orchestrateOptions: {}, + }) + + await drainStream(stream) + + expect(write).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'error', + error: 'resume failed', + }) + ) + expect(write.mock.invocationCallOrder.at(-1)).toBeLessThan(close.mock.invocationCallOrder[0]) + }) + + it('writes the thrown terminal error event before close for replay durability', async () => { + orchestrateCopilotStream.mockRejectedValue(new Error('kaboom')) + + const stream = createSSEStream({ + requestPayload: { message: 'hello' }, + userId: 'user-1', + streamId: 'stream-1', + executionId: 'exec-1', + runId: 'run-1', + currentChat: null, + isNewChat: false, + message: 'hello', + titleModel: 'gpt-5.4', + requestId: 'req-1', + orchestrateOptions: {}, + }) + + await drainStream(stream) + + expect(write).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'error', + error: 'kaboom', + }) + ) + expect(write.mock.invocationCallOrder.at(-1)).toBeLessThan(close.mock.invocationCallOrder[0]) + }) +}) diff --git a/apps/sim/lib/copilot/chat-streaming.ts b/apps/sim/lib/copilot/chat-streaming.ts index eeef50be0c8..67144d66a57 100644 --- a/apps/sim/lib/copilot/chat-streaming.ts +++ b/apps/sim/lib/copilot/chat-streaming.ts @@ -284,6 +284,13 @@ export function createSSEStream(params: StreamingOrchestrationParams): ReadableS logger.error(`[${requestId}] Orchestration returned failure`, { error: errorMessage, }) + await pushEvent({ + type: 'error', + error: errorMessage, + data: { + displayMessage: errorMessage, + }, + }) await eventWriter.close() await setStreamMeta(streamId, { status: 'error', @@ -316,24 +323,26 @@ export function createSSEStream(params: StreamingOrchestrationParams): ReadableS }) } logger.error(`[${requestId}] Orchestration error:`, error) + const errorMessage = error instanceof Error ? error.message : 'Stream error' + await pushEvent({ + type: 'error', + error: errorMessage, + data: { + displayMessage: 'An unexpected error occurred while processing the response.', + }, + }) await eventWriter.close() await setStreamMeta(streamId, { status: 'error', userId, executionId, runId, - error: error instanceof Error ? error.message : 'Stream error', + error: errorMessage, }) await updateRunStatus(runId, 'error', { completedAt: new Date(), - error: error instanceof Error ? error.message : 'Stream error', + error: errorMessage, }).catch(() => {}) - await pushEvent({ - type: 'error', - data: { - displayMessage: 'An unexpected error occurred while processing the response.', - }, - }) } finally { clearInterval(keepaliveInterval) activeStreams.delete(streamId) diff --git a/apps/sim/lib/copilot/orchestrator/index.test.ts b/apps/sim/lib/copilot/orchestrator/index.test.ts new file mode 100644 index 00000000000..7165c6d3017 --- /dev/null +++ b/apps/sim/lib/copilot/orchestrator/index.test.ts @@ -0,0 +1,141 @@ +/** + * @vitest-environment node + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + prepareExecutionContext, + getEffectiveDecryptedEnv, + runStreamLoop, + claimCompletedAsyncToolCall, + getAsyncToolCalls, + markAsyncToolResumed, + updateRunStatus, +} = vi.hoisted(() => ({ + prepareExecutionContext: vi.fn(), + getEffectiveDecryptedEnv: vi.fn(), + runStreamLoop: vi.fn(), + claimCompletedAsyncToolCall: vi.fn(), + getAsyncToolCalls: vi.fn(), + markAsyncToolResumed: vi.fn(), + updateRunStatus: vi.fn(), +})) + +vi.mock('@/lib/copilot/orchestrator/tool-executor', () => ({ + prepareExecutionContext, +})) + +vi.mock('@/lib/environment/utils', () => ({ + getEffectiveDecryptedEnv, +})) + +vi.mock('@/lib/copilot/async-runs/repository', () => ({ + claimCompletedAsyncToolCall, + getAsyncToolCalls, + markAsyncToolResumed, + updateRunStatus, +})) + +vi.mock('@/lib/copilot/orchestrator/stream/core', async () => { + const actual = await vi.importActual('./stream/core') + return { + ...actual, + buildToolCallSummaries: vi.fn(() => []), + runStreamLoop, + } +}) + +import { orchestrateCopilotStream } from './index' + +describe('orchestrateCopilotStream async continuation', () => { + beforeEach(() => { + vi.clearAllMocks() + prepareExecutionContext.mockResolvedValue({ + userId: 'user-1', + workflowId: 'workflow-1', + chatId: 'chat-1', + }) + getEffectiveDecryptedEnv.mockResolvedValue({}) + claimCompletedAsyncToolCall.mockResolvedValue({ toolCallId: 'tool-1' }) + getAsyncToolCalls.mockResolvedValue([ + { + toolCallId: 'tool-1', + toolName: 'read', + status: 'completed', + result: { ok: true }, + error: null, + }, + ]) + markAsyncToolResumed.mockResolvedValue(null) + updateRunStatus.mockResolvedValue(null) + }) + + it('builds resumed tool payloads with success=true for claimed completed rows', async () => { + runStreamLoop + .mockImplementationOnce(async (_url: string, _opts: RequestInit, context: any) => { + context.awaitingAsyncContinuation = { + checkpointId: 'checkpoint-1', + runId: 'run-1', + pendingToolCallIds: ['tool-1'], + } + }) + .mockImplementationOnce(async (url: string, opts: RequestInit) => { + expect(url).toContain('/api/tools/resume') + const body = JSON.parse(String(opts.body)) + expect(body).toEqual({ + checkpointId: 'checkpoint-1', + results: [ + { + callId: 'tool-1', + name: 'read', + data: { ok: true }, + success: true, + }, + ], + }) + }) + + const result = await orchestrateCopilotStream( + { message: 'hello' }, + { + userId: 'user-1', + workflowId: 'workflow-1', + chatId: 'chat-1', + executionId: 'exec-1', + runId: 'run-1', + } + ) + + expect(result.success).toBe(true) + expect(markAsyncToolResumed).toHaveBeenCalledWith('tool-1') + }) + + it('marks claimed tool calls resumed even when the resumed stream later records errors', async () => { + runStreamLoop + .mockImplementationOnce(async (_url: string, _opts: RequestInit, context: any) => { + context.awaitingAsyncContinuation = { + checkpointId: 'checkpoint-1', + runId: 'run-1', + pendingToolCallIds: ['tool-1'], + } + }) + .mockImplementationOnce(async (_url: string, _opts: RequestInit, context: any) => { + context.errors.push('resume stream failed after handoff') + }) + + const result = await orchestrateCopilotStream( + { message: 'hello' }, + { + userId: 'user-1', + workflowId: 'workflow-1', + chatId: 'chat-1', + executionId: 'exec-1', + runId: 'run-1', + } + ) + + expect(result.success).toBe(false) + expect(markAsyncToolResumed).toHaveBeenCalledWith('tool-1') + }) +}) diff --git a/apps/sim/lib/copilot/orchestrator/index.ts b/apps/sim/lib/copilot/orchestrator/index.ts index 0c7387dd04a..cb1c3e54f8d 100644 --- a/apps/sim/lib/copilot/orchestrator/index.ts +++ b/apps/sim/lib/copilot/orchestrator/index.ts @@ -20,6 +20,40 @@ import { buildToolCallSummaries, createStreamingContext, runStreamLoop } from '. const logger = createLogger('CopilotOrchestrator') +function didAsyncToolSucceed(input: { + durableStatus?: string | null + durableResult?: Record + durableError?: string | null + completion?: { status: string } | undefined + toolStateSuccess?: boolean | undefined +}) { + const { durableStatus, durableResult, durableError, completion, toolStateSuccess } = input + + if (durableStatus === ASYNC_TOOL_STATUS.completed) { + return true + } + if (durableStatus === ASYNC_TOOL_STATUS.failed || durableStatus === ASYNC_TOOL_STATUS.cancelled) { + return false + } + + if ( + durableStatus === ASYNC_TOOL_STATUS.resumeEnqueued || + durableStatus === ASYNC_TOOL_STATUS.resumed + ) { + if (durableError) { + return false + } + if (durableResult?.cancelled === true || typeof durableResult?.error === 'string') { + return false + } + if (durableResult) { + return true + } + } + + return completion?.status === 'success' || toolStateSuccess === true +} + export interface OrchestrateStreamOptions extends OrchestratorOptions { userId: string workflowId?: string @@ -119,6 +153,14 @@ export async function orchestrateCopilotStream( loopOptions ) + if (claimedToolCallIds.length > 0) { + logger.info('Marking async tool calls as resumed', { toolCallIds: claimedToolCallIds }) + await Promise.all( + claimedToolCallIds.map((toolCallId) => markAsyncToolResumed(toolCallId).catch(() => null)) + ) + claimedToolCallIds = [] + } + if (options.abortSignal?.aborted || context.wasAborted) { context.awaitingAsyncContinuation = undefined break @@ -177,16 +219,17 @@ export async function orchestrateCopilotStream( const durable = durableByToolCallId.get(toolCallId) const durableStatus = durable?.status - const success = - durableStatus === ASYNC_TOOL_STATUS.completed || - (durableStatus == null && - (completion?.status === 'success' || - (!completion && toolState?.result?.success === true))) - const durableResult = durable?.result && typeof durable.result === 'object' ? (durable.result as Record) : undefined + const success = didAsyncToolSucceed({ + durableStatus, + durableResult, + durableError: durable?.error, + completion, + toolStateSuccess: toolState?.result?.success, + }) const data = durableResult || completion?.data || @@ -232,12 +275,6 @@ export async function orchestrateCopilotStream( usage: context.usage, cost: context.cost, } - if (result.success && claimedToolCallIds.length > 0) { - logger.info('Marking async tool calls as resumed', { toolCallIds: claimedToolCallIds }) - await Promise.all( - claimedToolCallIds.map((toolCallId) => markAsyncToolResumed(toolCallId).catch(() => null)) - ) - } await options.onComplete?.(result) return result } catch (error) { diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.test.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.test.ts index 05b57a66e90..c154aec70af 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.test.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.test.ts @@ -13,12 +13,26 @@ const { executeToolServerSide, markToolComplete, isToolAvailableOnSimSide } = vi isToolAvailableOnSimSide: vi.fn().mockReturnValue(true), })) +const { upsertAsyncToolCall } = vi.hoisted(() => ({ + upsertAsyncToolCall: vi.fn(), +})) + vi.mock('@/lib/copilot/orchestrator/tool-executor', () => ({ executeToolServerSide, markToolComplete, isToolAvailableOnSimSide, })) +vi.mock('@/lib/copilot/async-runs/repository', async () => { + const actual = await vi.importActual( + '@/lib/copilot/async-runs/repository' + ) + return { + ...actual, + upsertAsyncToolCall, + } +}) + import { sseHandlers } from '@/lib/copilot/orchestrator/sse/handlers' import type { ExecutionContext, StreamingContext } from '@/lib/copilot/orchestrator/types' @@ -28,6 +42,7 @@ describe('sse-handlers tool lifecycle', () => { beforeEach(() => { vi.clearAllMocks() + upsertAsyncToolCall.mockResolvedValue(null) context = { chatId: undefined, messageId: 'msg-1', @@ -139,4 +154,26 @@ describe('sse-handlers tool lifecycle', () => { const updated = context.toolCalls.get('tool-cancel') expect(updated?.status).toBe('cancelled') }) + + it('still executes the tool when async row upsert fails', async () => { + upsertAsyncToolCall.mockRejectedValueOnce(new Error('db down')) + executeToolServerSide.mockResolvedValueOnce({ success: true, output: { ok: true } }) + markToolComplete.mockResolvedValueOnce(true) + + await sseHandlers.tool_call( + { + type: 'tool_call', + data: { id: 'tool-upsert-fail', name: 'read', arguments: { workflowId: 'workflow-1' } }, + } as any, + context, + execContext, + { onEvent: vi.fn(), interactive: false, timeout: 1000 } + ) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(executeToolServerSide).toHaveBeenCalledTimes(1) + expect(markToolComplete).toHaveBeenCalledTimes(1) + expect(context.toolCalls.get('tool-upsert-fail')?.status).toBe('success') + }) }) diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts index e4bbba660c3..a9ad474db14 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts @@ -348,12 +348,20 @@ export const sseHandlers: Record = { */ const fireToolExecution = () => { const pendingPromise = (async () => { - await upsertAsyncToolCall({ - runId: context.runId || crypto.randomUUID(), - toolCallId, - toolName, - args, - }) + try { + await upsertAsyncToolCall({ + runId: context.runId || crypto.randomUUID(), + toolCallId, + toolName, + args, + }) + } catch (err) { + logger.warn('Failed to persist async tool row before execution', { + toolCallId, + toolName, + error: err instanceof Error ? err.message : String(err), + }) + } return executeToolAndReport(toolCallId, context, execContext, options) })().catch((err) => { logger.error('Parallel tool execution failed', { @@ -566,12 +574,20 @@ export const subAgentHandlers: Record = { const fireToolExecution = () => { const pendingPromise = (async () => { - await upsertAsyncToolCall({ - runId: context.runId || crypto.randomUUID(), - toolCallId, - toolName, - args, - }) + try { + await upsertAsyncToolCall({ + runId: context.runId || crypto.randomUUID(), + toolCallId, + toolName, + args, + }) + } catch (err) { + logger.warn('Failed to persist async subagent tool row before execution', { + toolCallId, + toolName, + error: err instanceof Error ? err.message : String(err), + }) + } return executeToolAndReport(toolCallId, context, execContext, options) })().catch((err) => { logger.error('Parallel subagent tool execution failed', { From 1c0697ae1877f98f265c8c8e297cfc2a6382e800 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 16:00:11 -0700 Subject: [PATCH 23/31] Tool call loop --- .../sim/app/api/copilot/confirm/route.test.ts | 39 +- apps/sim/app/api/copilot/confirm/route.ts | 74 +- .../lib/copilot/async-runs/lifecycle.test.ts | 29 + apps/sim/lib/copilot/async-runs/lifecycle.ts | 24 +- .../lib/copilot/async-runs/repository.test.ts | 110 + apps/sim/lib/copilot/async-runs/repository.ts | 39 +- apps/sim/lib/copilot/constants.ts | 19 - .../lib/copilot/orchestrator/index.test.ts | 156 +- apps/sim/lib/copilot/orchestrator/index.ts | 82 +- .../copilot/orchestrator/persistence.test.ts | 110 + .../lib/copilot/orchestrator/persistence.ts | 71 +- .../sse/handlers/tool-execution.ts | 26 +- .../db/migrations/0180_amused_marvel_boy.sql | 6 + .../db/migrations/meta/0180_snapshot.json | 14430 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 3 +- 16 files changed, 15034 insertions(+), 191 deletions(-) create mode 100644 apps/sim/lib/copilot/async-runs/lifecycle.test.ts create mode 100644 apps/sim/lib/copilot/async-runs/repository.test.ts create mode 100644 apps/sim/lib/copilot/orchestrator/persistence.test.ts create mode 100644 packages/db/migrations/0180_amused_marvel_boy.sql create mode 100644 packages/db/migrations/meta/0180_snapshot.json diff --git a/apps/sim/app/api/copilot/confirm/route.test.ts b/apps/sim/app/api/copilot/confirm/route.test.ts index ee501f6739a..8570d637646 100644 --- a/apps/sim/app/api/copilot/confirm/route.test.ts +++ b/apps/sim/app/api/copilot/confirm/route.test.ts @@ -15,7 +15,6 @@ const { getRunSegment, upsertAsyncToolCall, completeAsyncToolCall, - getRedisClient, publishToolConfirmation, } = vi.hoisted(() => ({ authenticateCopilotRequestSessionOnly: vi.fn(), @@ -36,7 +35,6 @@ const { getRunSegment: vi.fn(), upsertAsyncToolCall: vi.fn(), completeAsyncToolCall: vi.fn(), - getRedisClient: vi.fn(), publishToolConfirmation: vi.fn(), })) @@ -56,10 +54,6 @@ vi.mock('@/lib/copilot/async-runs/repository', () => ({ completeAsyncToolCall, })) -vi.mock('@/lib/core/config/redis', () => ({ - getRedisClient, -})) - vi.mock('@/lib/copilot/orchestrator/persistence', () => ({ publishToolConfirmation, })) @@ -85,9 +79,6 @@ describe('Copilot Confirm API Route', () => { getRunSegment.mockResolvedValue({ id: 'run-1', userId: 'user-1' }) upsertAsyncToolCall.mockResolvedValue(existingRow) completeAsyncToolCall.mockResolvedValue(existingRow) - getRedisClient.mockReturnValue({ - set: vi.fn().mockResolvedValue('OK'), - }) }) function createMockPostRequest(body: Record): NextRequest { @@ -144,9 +135,6 @@ describe('Copilot Confirm API Route', () => { }) it('persists terminal confirmations through completeAsyncToolCall', async () => { - const redisSet = vi.fn().mockResolvedValue('OK') - getRedisClient.mockReturnValue({ set: redisSet }) - const response = await POST( createMockPostRequest({ toolCallId: 'tool-call-123', @@ -164,7 +152,6 @@ describe('Copilot Confirm API Route', () => { error: null, }) expect(upsertAsyncToolCall).not.toHaveBeenCalled() - expect(redisSet).toHaveBeenCalledOnce() expect(publishToolConfirmation).toHaveBeenCalledWith( expect.objectContaining({ toolCallId: 'tool-call-123', @@ -194,9 +181,7 @@ describe('Copilot Confirm API Route', () => { expect(completeAsyncToolCall).not.toHaveBeenCalled() }) - it('publishes confirmation even when Redis is unavailable', async () => { - getRedisClient.mockReturnValue(null) - + it('publishes confirmation after a durable non-terminal update', async () => { const response = await POST( createMockPostRequest({ toolCallId: 'tool-call-123', @@ -205,6 +190,14 @@ describe('Copilot Confirm API Route', () => { ) expect(response.status).toBe(200) + expect(upsertAsyncToolCall).toHaveBeenCalledWith({ + runId: 'run-1', + checkpointId: 'checkpoint-1', + toolCallId: 'tool-call-123', + toolName: 'client_tool', + args: { foo: 'bar' }, + status: 'pending', + }) expect(publishToolConfirmation).toHaveBeenCalledWith( expect.objectContaining({ toolCallId: 'tool-call-123', @@ -212,4 +205,18 @@ describe('Copilot Confirm API Route', () => { }) ) }) + + it('returns 400 when the durable write fails before publish', async () => { + completeAsyncToolCall.mockRejectedValueOnce(new Error('db down')) + + const response = await POST( + createMockPostRequest({ + toolCallId: 'tool-call-123', + status: 'success', + }) + ) + + expect(response.status).toBe(400) + expect(publishToolConfirmation).not.toHaveBeenCalled() + }) }) diff --git a/apps/sim/app/api/copilot/confirm/route.ts b/apps/sim/app/api/copilot/confirm/route.ts index a49c628f9ed..bf154487f05 100644 --- a/apps/sim/app/api/copilot/confirm/route.ts +++ b/apps/sim/app/api/copilot/confirm/route.ts @@ -7,7 +7,6 @@ import { getRunSegment, upsertAsyncToolCall, } from '@/lib/copilot/async-runs/repository' -import { REDIS_TOOL_CALL_PREFIX, REDIS_TOOL_CALL_TTL_SECONDS } from '@/lib/copilot/constants' import { publishToolConfirmation } from '@/lib/copilot/orchestrator/persistence' import { authenticateCopilotRequestSessionOnly, @@ -18,7 +17,6 @@ import { createUnauthorizedResponse, type NotificationStatus, } from '@/lib/copilot/request-helpers' -import { getRedisClient } from '@/lib/core/config/redis' const logger = createLogger('CopilotConfirmAPI') @@ -33,8 +31,7 @@ const ConfirmationSchema = z.object({ }) /** - * Write the user's tool decision to Redis. The server-side orchestrator's - * waitForToolDecision() polls Redis for this value. + * Persist the durable tool status, then publish a wakeup event. */ async function updateToolCallStatus( existing: NonNullable>>, @@ -51,57 +48,34 @@ async function updateToolCallStatus( : status === 'error' || status === 'rejected' ? 'failed' : 'pending' - if ( - durableStatus === 'completed' || - durableStatus === 'failed' || - durableStatus === 'cancelled' - ) { - await completeAsyncToolCall({ - toolCallId, - status: durableStatus, - result: data ?? null, - error: status === 'success' ? null : message || status, - }).catch(() => {}) - } else if (existing.runId) { - await upsertAsyncToolCall({ - runId: existing.runId, - checkpointId: existing.checkpointId ?? null, - toolCallId, - toolName: existing.toolName || 'client_tool', - args: (existing.args as Record | null) ?? {}, - status: durableStatus, - }).catch(() => {}) - } - - const redis = getRedisClient() - if (!redis) { - logger.warn('Redis client not available for tool confirmation; durable DB mirror only') - publishToolConfirmation({ - toolCallId, - status, - message: message || undefined, - timestamp: new Date().toISOString(), - data, - }) - return true - } - try { - const key = `${REDIS_TOOL_CALL_PREFIX}${toolCallId}` - const payload: Record = { - status, - message: message || null, - timestamp: new Date().toISOString(), - } - if (data) { - payload.data = data + if ( + durableStatus === 'completed' || + durableStatus === 'failed' || + durableStatus === 'cancelled' + ) { + await completeAsyncToolCall({ + toolCallId, + status: durableStatus, + result: data ?? null, + error: status === 'success' ? null : message || status, + }) + } else if (existing.runId) { + await upsertAsyncToolCall({ + runId: existing.runId, + checkpointId: existing.checkpointId ?? null, + toolCallId, + toolName: existing.toolName || 'client_tool', + args: (existing.args as Record | null) ?? {}, + status: durableStatus, + }) } - await redis.set(key, JSON.stringify(payload), 'EX', REDIS_TOOL_CALL_TTL_SECONDS) + const timestamp = new Date().toISOString() publishToolConfirmation({ toolCallId, status, message: message || undefined, - timestamp: payload.timestamp as string, + timestamp, data, }) return true @@ -147,7 +121,7 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } - // Update the tool call status in Redis + // Update the durable tool call status and wake any waiters. const updated = await updateToolCallStatus(existing, status, message, data) if (!updated) { diff --git a/apps/sim/lib/copilot/async-runs/lifecycle.test.ts b/apps/sim/lib/copilot/async-runs/lifecycle.test.ts new file mode 100644 index 00000000000..bc6668debf6 --- /dev/null +++ b/apps/sim/lib/copilot/async-runs/lifecycle.test.ts @@ -0,0 +1,29 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from 'vitest' +import { + ASYNC_TOOL_STATUS, + inferDeliveredAsyncSuccess, + isDeliveredAsyncStatus, + isTerminalAsyncStatus, +} from './lifecycle' + +describe('async tool lifecycle helpers', () => { + it('treats only completed, failed, and cancelled as terminal execution states', () => { + expect(isTerminalAsyncStatus(ASYNC_TOOL_STATUS.pending)).toBe(false) + expect(isTerminalAsyncStatus(ASYNC_TOOL_STATUS.running)).toBe(false) + expect(isTerminalAsyncStatus(ASYNC_TOOL_STATUS.completed)).toBe(true) + expect(isTerminalAsyncStatus(ASYNC_TOOL_STATUS.failed)).toBe(true) + expect(isTerminalAsyncStatus(ASYNC_TOOL_STATUS.cancelled)).toBe(true) + expect(isTerminalAsyncStatus(ASYNC_TOOL_STATUS.delivered)).toBe(false) + }) + + it('treats delivered rows as success unless durable error/cancel markers say otherwise', () => { + expect(isDeliveredAsyncStatus(ASYNC_TOOL_STATUS.delivered)).toBe(true) + expect(inferDeliveredAsyncSuccess({ result: { ok: true }, error: null })).toBe(true) + expect(inferDeliveredAsyncSuccess({ result: { cancelled: true }, error: null })).toBe(false) + expect(inferDeliveredAsyncSuccess({ result: null, error: 'tool failed' })).toBe(false) + }) +}) diff --git a/apps/sim/lib/copilot/async-runs/lifecycle.ts b/apps/sim/lib/copilot/async-runs/lifecycle.ts index 47bb77809a7..949ff773247 100644 --- a/apps/sim/lib/copilot/async-runs/lifecycle.ts +++ b/apps/sim/lib/copilot/async-runs/lifecycle.ts @@ -6,8 +6,7 @@ export const ASYNC_TOOL_STATUS = { completed: 'completed', failed: 'failed', cancelled: 'cancelled', - resumeEnqueued: 'resume_enqueued', - resumed: 'resumed', + delivered: 'delivered', } as const export type AsyncLifecycleStatus = @@ -22,6 +21,8 @@ export type AsyncTerminalStatus = | typeof ASYNC_TOOL_STATUS.failed | typeof ASYNC_TOOL_STATUS.cancelled +export type AsyncFinishedStatus = AsyncTerminalStatus | typeof ASYNC_TOOL_STATUS.delivered + export interface AsyncCompletionEnvelope { toolCallId: string status: string @@ -43,3 +44,22 @@ export function isTerminalAsyncStatus( status === ASYNC_TOOL_STATUS.cancelled ) } + +export function isDeliveredAsyncStatus( + status: CopilotAsyncToolStatus | string | null | undefined +): status is typeof ASYNC_TOOL_STATUS.delivered { + return status === ASYNC_TOOL_STATUS.delivered +} + +export function inferDeliveredAsyncSuccess(input: { + result?: Record | null + error?: string | null +}) { + if (input.error) return false + const result = input.result ?? undefined + if (!result) return true + if (result.cancelled === true || result.cancelledByUser === true) return false + if (typeof result.reason === 'string' && result.reason === 'user_cancelled') return false + if (typeof result.error === 'string') return false + return true +} diff --git a/apps/sim/lib/copilot/async-runs/repository.test.ts b/apps/sim/lib/copilot/async-runs/repository.test.ts new file mode 100644 index 00000000000..2ed15671456 --- /dev/null +++ b/apps/sim/lib/copilot/async-runs/repository.test.ts @@ -0,0 +1,110 @@ +/** + * @vitest-environment node + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockSelectLimit, mockUpdateSet, mockUpdateWhere, mockUpdateReturning } = vi.hoisted(() => ({ + mockSelectLimit: vi.fn(), + mockUpdateSet: vi.fn(), + mockUpdateWhere: vi.fn(), + mockUpdateReturning: vi.fn(), +})) + +vi.mock('@sim/db', () => ({ + db: { + select: () => ({ + from: () => ({ + where: () => ({ + limit: mockSelectLimit, + }), + }), + }), + update: () => ({ + set: mockUpdateSet, + }), + insert: vi.fn(), + }, +})) + +mockUpdateSet.mockImplementation(() => ({ + where: mockUpdateWhere, +})) + +mockUpdateWhere.mockImplementation(() => ({ + returning: mockUpdateReturning, +})) + +import { + claimCompletedAsyncToolCall, + completeAsyncToolCall, + markAsyncToolDelivered, +} from './repository' + +describe('async tool repository single-row semantics', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('does not overwrite a delivered row on late completion', async () => { + const deliveredRow = { + toolCallId: 'tool-1', + status: 'delivered', + result: { ok: true }, + error: null, + } + mockSelectLimit.mockResolvedValueOnce([deliveredRow]) + + const result = await completeAsyncToolCall({ + toolCallId: 'tool-1', + status: 'completed', + result: { ok: false }, + error: null, + }) + + expect(result).toEqual(deliveredRow) + expect(mockUpdateReturning).not.toHaveBeenCalled() + }) + + it('marks a row delivered and clears the claim fields', async () => { + mockUpdateReturning.mockResolvedValueOnce([ + { + toolCallId: 'tool-1', + status: 'delivered', + }, + ]) + + await markAsyncToolDelivered('tool-1') + + expect(mockUpdateSet).toHaveBeenCalledWith( + expect.objectContaining({ + status: 'delivered', + claimedBy: null, + claimedAt: null, + }) + ) + }) + + it('claims only completed rows for delivery handoff', async () => { + mockUpdateReturning.mockResolvedValueOnce([ + { + toolCallId: 'tool-1', + status: 'completed', + claimedBy: 'worker-1', + }, + ]) + + const result = await claimCompletedAsyncToolCall('tool-1', 'worker-1') + + expect(result).toEqual({ + toolCallId: 'tool-1', + status: 'completed', + claimedBy: 'worker-1', + }) + expect(mockUpdateSet).toHaveBeenCalledWith( + expect.objectContaining({ + claimedBy: 'worker-1', + }) + ) + }) +}) diff --git a/apps/sim/lib/copilot/async-runs/repository.ts b/apps/sim/lib/copilot/async-runs/repository.ts index 4f636be4f68..07d0a3d85da 100644 --- a/apps/sim/lib/copilot/async-runs/repository.ts +++ b/apps/sim/lib/copilot/async-runs/repository.ts @@ -8,7 +8,7 @@ import { } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, desc, eq, inArray, isNull } from 'drizzle-orm' -import { isTerminalAsyncStatus } from './lifecycle' +import { ASYNC_TOOL_STATUS, isDeliveredAsyncStatus, isTerminalAsyncStatus } from './lifecycle' const logger = createLogger('CopilotAsyncRunsRepo') @@ -224,11 +224,7 @@ export async function completeAsyncToolCall(input: { return null } - if ( - isTerminalAsyncStatus(existing.status) || - existing.status === 'resume_enqueued' || - existing.status === 'resumed' - ) { + if (isTerminalAsyncStatus(existing.status) || isDeliveredAsyncStatus(existing.status)) { return existing } @@ -241,12 +237,11 @@ export async function completeAsyncToolCall(input: { }) } -export async function enqueueAsyncToolResume(toolCallId: string) { - return markAsyncToolStatus(toolCallId, 'resume_enqueued') -} - -export async function markAsyncToolResumed(toolCallId: string) { - return markAsyncToolStatus(toolCallId, 'resumed') +export async function markAsyncToolDelivered(toolCallId: string) { + return markAsyncToolStatus(toolCallId, ASYNC_TOOL_STATUS.delivered, { + claimedBy: null, + claimedAt: null, + }) } export async function listAsyncToolCallsForRun(runId: string) { @@ -284,3 +279,23 @@ export async function claimCompletedAsyncToolCall(toolCallId: string, workerId: return row ?? null } + +export async function releaseCompletedAsyncToolClaim(toolCallId: string, workerId: string) { + const [row] = await db + .update(copilotAsyncToolCalls) + .set({ + claimedBy: null, + claimedAt: null, + updatedAt: new Date(), + }) + .where( + and( + eq(copilotAsyncToolCalls.toolCallId, toolCallId), + inArray(copilotAsyncToolCalls.status, ['completed', 'failed', 'cancelled']), + eq(copilotAsyncToolCalls.claimedBy, workerId) + ) + ) + .returning() + + return row ?? null +} diff --git a/apps/sim/lib/copilot/constants.ts b/apps/sim/lib/copilot/constants.ts index 09359f52ca2..fbf4e312957 100644 --- a/apps/sim/lib/copilot/constants.ts +++ b/apps/sim/lib/copilot/constants.ts @@ -14,9 +14,6 @@ export const SIM_AGENT_API_URL = // Redis key prefixes // --------------------------------------------------------------------------- -/** Redis key prefix for tool call confirmation payloads (polled by waitForToolDecision). */ -export const REDIS_TOOL_CALL_PREFIX = 'tool_call:' - /** Redis key prefix for copilot SSE stream buffers. */ export const REDIS_COPILOT_STREAM_PREFIX = 'copilot_stream:' @@ -30,22 +27,6 @@ export const ORCHESTRATION_TIMEOUT_MS = 3_600_000 /** Timeout for the client-side streaming response handler (60 min). */ export const STREAM_TIMEOUT_MS = 3_600_000 -/** TTL for Redis tool call confirmation entries (24 h). */ -export const REDIS_TOOL_CALL_TTL_SECONDS = 86_400 - -// --------------------------------------------------------------------------- -// Tool decision polling -// --------------------------------------------------------------------------- - -/** Initial poll interval when waiting for a user tool decision. */ -export const TOOL_DECISION_INITIAL_POLL_MS = 100 - -/** Maximum poll interval when waiting for a user tool decision. */ -export const TOOL_DECISION_MAX_POLL_MS = 3_000 - -/** Backoff multiplier for the tool decision poll interval. */ -export const TOOL_DECISION_POLL_BACKOFF = 1.5 - // --------------------------------------------------------------------------- // Stream resume // --------------------------------------------------------------------------- diff --git a/apps/sim/lib/copilot/orchestrator/index.test.ts b/apps/sim/lib/copilot/orchestrator/index.test.ts index 7165c6d3017..5a1c30c6206 100644 --- a/apps/sim/lib/copilot/orchestrator/index.test.ts +++ b/apps/sim/lib/copilot/orchestrator/index.test.ts @@ -3,22 +3,27 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { OrchestratorOptions } from './types' const { prepareExecutionContext, getEffectiveDecryptedEnv, runStreamLoop, claimCompletedAsyncToolCall, + getAsyncToolCall, getAsyncToolCalls, - markAsyncToolResumed, + markAsyncToolDelivered, + releaseCompletedAsyncToolClaim, updateRunStatus, } = vi.hoisted(() => ({ prepareExecutionContext: vi.fn(), getEffectiveDecryptedEnv: vi.fn(), runStreamLoop: vi.fn(), claimCompletedAsyncToolCall: vi.fn(), + getAsyncToolCall: vi.fn(), getAsyncToolCalls: vi.fn(), - markAsyncToolResumed: vi.fn(), + markAsyncToolDelivered: vi.fn(), + releaseCompletedAsyncToolClaim: vi.fn(), updateRunStatus: vi.fn(), })) @@ -32,8 +37,10 @@ vi.mock('@/lib/environment/utils', () => ({ vi.mock('@/lib/copilot/async-runs/repository', () => ({ claimCompletedAsyncToolCall, + getAsyncToolCall, getAsyncToolCalls, - markAsyncToolResumed, + markAsyncToolDelivered, + releaseCompletedAsyncToolClaim, updateRunStatus, })) @@ -58,6 +65,13 @@ describe('orchestrateCopilotStream async continuation', () => { }) getEffectiveDecryptedEnv.mockResolvedValue({}) claimCompletedAsyncToolCall.mockResolvedValue({ toolCallId: 'tool-1' }) + getAsyncToolCall.mockResolvedValue({ + toolCallId: 'tool-1', + toolName: 'read', + status: 'completed', + result: { ok: true }, + error: null, + }) getAsyncToolCalls.mockResolvedValue([ { toolCallId: 'tool-1', @@ -67,11 +81,12 @@ describe('orchestrateCopilotStream async continuation', () => { error: null, }, ]) - markAsyncToolResumed.mockResolvedValue(null) + markAsyncToolDelivered.mockResolvedValue(null) + releaseCompletedAsyncToolClaim.mockResolvedValue(null) updateRunStatus.mockResolvedValue(null) }) - it('builds resumed tool payloads with success=true for claimed completed rows', async () => { + it('builds resume payloads with success=true for claimed completed rows', async () => { runStreamLoop .mockImplementationOnce(async (_url: string, _opts: RequestInit, context: any) => { context.awaitingAsyncContinuation = { @@ -108,10 +123,10 @@ describe('orchestrateCopilotStream async continuation', () => { ) expect(result.success).toBe(true) - expect(markAsyncToolResumed).toHaveBeenCalledWith('tool-1') + expect(markAsyncToolDelivered).toHaveBeenCalledWith('tool-1') }) - it('marks claimed tool calls resumed even when the resumed stream later records errors', async () => { + it('marks claimed tool calls delivered even when the resumed stream later records errors', async () => { runStreamLoop .mockImplementationOnce(async (_url: string, _opts: RequestInit, context: any) => { context.awaitingAsyncContinuation = { @@ -136,6 +151,131 @@ describe('orchestrateCopilotStream async continuation', () => { ) expect(result.success).toBe(false) - expect(markAsyncToolResumed).toHaveBeenCalledWith('tool-1') + expect(markAsyncToolDelivered).toHaveBeenCalledWith('tool-1') + }) + + it('forwards done events while still marking async pauses on the run', async () => { + const onEvent = vi.fn() + const streamOptions: OrchestratorOptions = { onEvent } + runStreamLoop.mockImplementationOnce( + async (_url: string, _opts: RequestInit, _context: any, _exec: any, loopOptions: any) => { + await loopOptions.onEvent({ + type: 'done', + data: { + response: { + async_pause: { + checkpointId: 'checkpoint-1', + runId: 'run-1', + }, + }, + }, + }) + } + ) + + await orchestrateCopilotStream( + { message: 'hello' }, + { + userId: 'user-1', + workflowId: 'workflow-1', + chatId: 'chat-1', + executionId: 'exec-1', + runId: 'run-1', + ...streamOptions, + } + ) + + expect(onEvent).toHaveBeenCalledWith(expect.objectContaining({ type: 'done' })) + expect(updateRunStatus).toHaveBeenCalledWith('run-1', 'paused_waiting_for_tool') + }) + + it('waits for a local running tool before retrying the claim', async () => { + const localPendingPromise = Promise.resolve({ + status: 'success', + data: { ok: true }, + }) + + claimCompletedAsyncToolCall + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ toolCallId: 'tool-1' }) + getAsyncToolCall + .mockResolvedValueOnce({ + toolCallId: 'tool-1', + toolName: 'read', + status: 'running', + result: null, + error: null, + }) + .mockResolvedValue({ + toolCallId: 'tool-1', + toolName: 'read', + status: 'completed', + result: { ok: true }, + error: null, + }) + + runStreamLoop + .mockImplementationOnce(async (_url: string, _opts: RequestInit, context: any) => { + context.awaitingAsyncContinuation = { + checkpointId: 'checkpoint-1', + runId: 'run-1', + pendingToolCallIds: ['tool-1'], + } + context.pendingToolPromises.set('tool-1', localPendingPromise) + }) + .mockImplementationOnce(async () => {}) + .mockImplementationOnce(async (url: string, opts: RequestInit) => { + expect(url).toContain('/api/tools/resume') + const body = JSON.parse(String(opts.body)) + expect(body.results[0]).toEqual({ + callId: 'tool-1', + name: 'read', + data: { ok: true }, + success: true, + }) + }) + + const result = await orchestrateCopilotStream( + { message: 'hello' }, + { + userId: 'user-1', + workflowId: 'workflow-1', + chatId: 'chat-1', + executionId: 'exec-1', + runId: 'run-1', + } + ) + + expect(result.success).toBe(true) + expect(markAsyncToolDelivered).toHaveBeenCalledWith('tool-1') + }) + + it('releases claimed rows if the resume stream throws before delivery is marked', async () => { + runStreamLoop + .mockImplementationOnce(async (_url: string, _opts: RequestInit, context: any) => { + context.awaitingAsyncContinuation = { + checkpointId: 'checkpoint-1', + runId: 'run-1', + pendingToolCallIds: ['tool-1'], + } + }) + .mockImplementationOnce(async () => { + throw new Error('resume failed') + }) + + const result = await orchestrateCopilotStream( + { message: 'hello' }, + { + userId: 'user-1', + workflowId: 'workflow-1', + chatId: 'chat-1', + executionId: 'exec-1', + runId: 'run-1', + } + ) + + expect(result.success).toBe(false) + expect(releaseCompletedAsyncToolClaim).toHaveBeenCalledWith('tool-1', 'run-1') + expect(markAsyncToolDelivered).not.toHaveBeenCalled() }) }) diff --git a/apps/sim/lib/copilot/orchestrator/index.ts b/apps/sim/lib/copilot/orchestrator/index.ts index cb1c3e54f8d..8782a2870e0 100644 --- a/apps/sim/lib/copilot/orchestrator/index.ts +++ b/apps/sim/lib/copilot/orchestrator/index.ts @@ -1,9 +1,16 @@ import { createLogger } from '@sim/logger' -import { ASYNC_TOOL_STATUS, isTerminalAsyncStatus } from '@/lib/copilot/async-runs/lifecycle' +import { + ASYNC_TOOL_STATUS, + inferDeliveredAsyncSuccess, + isDeliveredAsyncStatus, + isTerminalAsyncStatus, +} from '@/lib/copilot/async-runs/lifecycle' import { claimCompletedAsyncToolCall, + getAsyncToolCall, getAsyncToolCalls, - markAsyncToolResumed, + markAsyncToolDelivered, + releaseCompletedAsyncToolClaim, updateRunStatus, } from '@/lib/copilot/async-runs/repository' import { SIM_AGENT_API_URL, SIM_AGENT_VERSION } from '@/lib/copilot/constants' @@ -36,19 +43,11 @@ function didAsyncToolSucceed(input: { return false } - if ( - durableStatus === ASYNC_TOOL_STATUS.resumeEnqueued || - durableStatus === ASYNC_TOOL_STATUS.resumed - ) { - if (durableError) { - return false - } - if (durableResult?.cancelled === true || typeof durableResult?.error === 'string') { - return false - } - if (durableResult) { - return true - } + if (durableStatus === ASYNC_TOOL_STATUS.delivered) { + return inferDeliveredAsyncSuccess({ + result: durableResult, + error: durableError, + }) } return completion?.status === 'success' || toolStateSuccess === true @@ -109,11 +108,12 @@ export async function orchestrateCopilotStream( runId, messageId: typeof payloadMsgId === 'string' ? payloadMsgId : crypto.randomUUID(), }) + let claimedToolCallIds: string[] = [] + let claimedByWorkerId: string | null = null try { let route = goRoute let payload = requestPayload - let claimedToolCallIds: string[] = [] const callerOnEvent = options.onEvent @@ -130,7 +130,6 @@ export async function orchestrateCopilotStream( if (runId) { await updateRunStatus(runId, 'paused_waiting_for_tool').catch(() => {}) } - return } } await callerOnEvent?.(event) @@ -154,11 +153,14 @@ export async function orchestrateCopilotStream( ) if (claimedToolCallIds.length > 0) { - logger.info('Marking async tool calls as resumed', { toolCallIds: claimedToolCallIds }) + logger.info('Marking async tool calls as delivered', { toolCallIds: claimedToolCallIds }) await Promise.all( - claimedToolCallIds.map((toolCallId) => markAsyncToolResumed(toolCallId).catch(() => null)) + claimedToolCallIds.map((toolCallId) => + markAsyncToolDelivered(toolCallId).catch(() => null) + ) ) claimedToolCallIds = [] + claimedByWorkerId = null } if (options.abortSignal?.aborted || context.wasAborted) { @@ -171,7 +173,9 @@ export async function orchestrateCopilotStream( claimedToolCallIds = [] const resumeWorkerId = continuation.runId || context.runId || context.messageId + claimedByWorkerId = resumeWorkerId const claimableToolCallIds: string[] = [] + const localPendingPromises: Promise[] = [] for (const toolCallId of continuation.pendingToolCallIds) { const claimed = await claimCompletedAsyncToolCall(toolCallId, resumeWorkerId).catch( () => null @@ -181,20 +185,31 @@ export async function orchestrateCopilotStream( claimedToolCallIds.push(toolCallId) continue } - // Fall back to local continuation if the durable row is missing, but do not - // retry rows another worker already claimed. - const localPending = context.pendingToolPromises.has(toolCallId) - if (localPending) { + const durableRow = await getAsyncToolCall(toolCallId).catch(() => null) + const localPendingPromise = context.pendingToolPromises.get(toolCallId) + if (!durableRow && localPendingPromise) { claimableToolCallIds.push(toolCallId) - } else { - logger.warn('Skipping already-claimed or missing async tool resume', { + continue + } + if (durableRow && durableRow.status === ASYNC_TOOL_STATUS.running && localPendingPromise) { + localPendingPromises.push(localPendingPromise) + logger.info('Waiting for local async tool completion before retrying resume claim', { toolCallId, runId: continuation.runId, }) + continue } + logger.warn('Skipping already-claimed or missing async tool resume', { + toolCallId, + runId: continuation.runId, + }) } if (claimableToolCallIds.length === 0) { + if (localPendingPromises.length > 0) { + await Promise.allSettled(localPendingPromises) + continue + } logger.warn('Skipping async resume because no tool calls were claimable', { checkpointId: continuation.checkpointId, runId: continuation.runId, @@ -240,7 +255,11 @@ export async function orchestrateCopilotStream( error: completion?.message || durable?.error || toolState?.error || 'Tool failed', }) - if (durableStatus && !isTerminalAsyncStatus(durableStatus)) { + if ( + durableStatus && + !isTerminalAsyncStatus(durableStatus) && + !isDeliveredAsyncStatus(durableStatus) + ) { logger.warn('Async tool row was claimed for resume without terminal durable state', { toolCallId, status: durableStatus, @@ -279,6 +298,17 @@ export async function orchestrateCopilotStream( return result } catch (error) { const err = error instanceof Error ? error : new Error('Copilot orchestration failed') + if (claimedToolCallIds.length > 0 && claimedByWorkerId) { + logger.warn('Releasing async tool claims after delivery failure', { + toolCallIds: claimedToolCallIds, + workerId: claimedByWorkerId, + }) + await Promise.all( + claimedToolCallIds.map((toolCallId) => + releaseCompletedAsyncToolClaim(toolCallId, claimedByWorkerId!).catch(() => null) + ) + ) + } logger.error('Copilot orchestration failed', { error: err.message }) await options.onError?.(err) return { diff --git a/apps/sim/lib/copilot/orchestrator/persistence.test.ts b/apps/sim/lib/copilot/orchestrator/persistence.test.ts new file mode 100644 index 00000000000..0474fab9f80 --- /dev/null +++ b/apps/sim/lib/copilot/orchestrator/persistence.test.ts @@ -0,0 +1,110 @@ +/** + * @vitest-environment node + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { getAsyncToolCalls } = vi.hoisted(() => ({ + getAsyncToolCalls: vi.fn(), +})) + +const channelHandlers = new Set<(event: any) => void>() + +vi.mock('@/lib/copilot/async-runs/repository', () => ({ + getAsyncToolCalls, +})) + +vi.mock('@/lib/events/pubsub', () => ({ + createPubSubChannel: () => ({ + publish(event: any) { + for (const handler of channelHandlers) handler(event) + }, + subscribe(handler: (event: any) => void) { + channelHandlers.add(handler) + return () => { + channelHandlers.delete(handler) + } + }, + dispose() {}, + }), +})) + +import { + getToolConfirmation, + publishToolConfirmation, + waitForToolConfirmation, +} from './persistence' + +describe('copilot orchestrator persistence', () => { + let row: { + status: string + error?: string | null + result?: Record | null + updatedAt: Date + } | null + + beforeEach(() => { + vi.clearAllMocks() + channelHandlers.clear() + row = null + getAsyncToolCalls.mockImplementation(async () => (row ? [row] : [])) + }) + + it('reads the durable DB row as the source of truth', async () => { + row = { + status: 'completed', + result: { ok: true }, + error: null, + updatedAt: new Date('2026-01-01T00:00:00.000Z'), + } + + await expect(getToolConfirmation('tool-1')).resolves.toEqual({ + status: 'success', + message: undefined, + data: { ok: true }, + timestamp: '2026-01-01T00:00:00.000Z', + }) + }) + + it('waits through intermediate events until the durable row becomes terminal', async () => { + row = { + status: 'pending', + error: null, + result: null, + updatedAt: new Date('2026-01-01T00:00:00.000Z'), + } + + const waitPromise = waitForToolConfirmation('tool-1', 5_000, undefined, { + acceptStatus: (status) => + status === 'success' || status === 'error' || status === 'cancelled', + }) + + publishToolConfirmation({ + toolCallId: 'tool-1', + status: 'accepted', + timestamp: '2026-01-01T00:00:01.000Z', + }) + + await Promise.resolve() + + row = { + status: 'completed', + error: null, + result: { ok: true }, + updatedAt: new Date('2026-01-01T00:00:02.000Z'), + } + + publishToolConfirmation({ + toolCallId: 'tool-1', + status: 'success', + timestamp: '2026-01-01T00:00:02.000Z', + }) + + await expect(waitPromise).resolves.toEqual({ + status: 'success', + message: undefined, + data: { ok: true }, + timestamp: '2026-01-01T00:00:02.000Z', + }) + }) +}) diff --git a/apps/sim/lib/copilot/orchestrator/persistence.ts b/apps/sim/lib/copilot/orchestrator/persistence.ts index 8dfc4075169..d0b47ecb8fc 100644 --- a/apps/sim/lib/copilot/orchestrator/persistence.ts +++ b/apps/sim/lib/copilot/orchestrator/persistence.ts @@ -1,8 +1,6 @@ import { createLogger } from '@sim/logger' import type { AsyncCompletionEnvelope } from '@/lib/copilot/async-runs/lifecycle' import { getAsyncToolCalls } from '@/lib/copilot/async-runs/repository' -import { REDIS_TOOL_CALL_PREFIX } from '@/lib/copilot/constants' -import { getRedisClient } from '@/lib/core/config/redis' import { createPubSubChannel } from '@/lib/events/pubsub' const logger = createLogger('CopilotOrchestratorPersistence') @@ -13,7 +11,7 @@ const toolConfirmationChannel = createPubSubChannel({ }) /** - * Get a tool call confirmation status from Redis. + * Get a tool call confirmation status from the durable async tool row. */ export async function getToolConfirmation(toolCallId: string): Promise<{ status: string @@ -21,33 +19,20 @@ export async function getToolConfirmation(toolCallId: string): Promise<{ timestamp?: string data?: Record } | null> { - const redis = getRedisClient() - if (!redis) { - const [row] = await getAsyncToolCalls([toolCallId]).catch(() => []) - if (!row) return null - return { - status: - row.status === 'completed' ? 'success' : row.status === 'failed' ? 'error' : row.status, - message: row.error || undefined, - data: (row.result as Record | null) || undefined, - } - } - - try { - const raw = await redis.get(`${REDIS_TOOL_CALL_PREFIX}${toolCallId}`) - if (!raw) return null - return JSON.parse(raw) as { - status: string - message?: string - timestamp?: string - data?: Record - } - } catch (error) { - logger.error('Failed to read tool confirmation', { - toolCallId, - error: error instanceof Error ? error.message : String(error), - }) - return null + const [row] = await getAsyncToolCalls([toolCallId]).catch(() => []) + if (!row) return null + return { + status: + row.status === 'completed' + ? 'success' + : row.status === 'failed' + ? 'error' + : row.status === 'cancelled' + ? 'cancelled' + : row.status, + message: row.error || undefined, + data: (row.result as Record | null) || undefined, + timestamp: row.updatedAt?.toISOString?.(), } } @@ -62,15 +47,19 @@ export function publishToolConfirmation(event: AsyncCompletionEnvelope): void { export async function waitForToolConfirmation( toolCallId: string, timeoutMs: number, - abortSignal?: AbortSignal + abortSignal?: AbortSignal, + options: { + acceptStatus?: (status: string) => boolean + } = {} ): Promise<{ status: string message?: string timestamp?: string data?: Record } | null> { + const acceptStatus = options.acceptStatus ?? (() => true) const existing = await getToolConfirmation(toolCallId) - if (existing) { + if (existing && acceptStatus(existing.status)) { logger.info('Resolved tool confirmation immediately', { toolCallId, status: existing.status, @@ -107,15 +96,13 @@ export async function waitForToolConfirmation( unsubscribe = toolConfirmationChannel.subscribe((event) => { if (event.toolCallId !== toolCallId) return - logger.info('Resolved tool confirmation from pubsub', { - toolCallId, - status: event.status, - }) - settle({ - status: event.status, - message: event.message, - timestamp: event.timestamp, - data: event.data, + void getToolConfirmation(toolCallId).then((latest) => { + if (!latest || !acceptStatus(latest.status)) return + logger.info('Resolved tool confirmation from pubsub', { + toolCallId, + status: latest.status, + }) + settle(latest) }) }) @@ -127,7 +114,7 @@ export async function waitForToolConfirmation( abortSignal?.addEventListener('abort', onAbort, { once: true }) void getToolConfirmation(toolCallId).then((latest) => { - if (latest) { + if (latest && acceptStatus(latest.status)) { logger.info('Resolved tool confirmation after subscribe', { toolCallId, status: latest.status, diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts index edfdd110218..4b430f7fb47 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts @@ -761,20 +761,9 @@ export async function executeToolAndReport( } } -export async function waitForToolDecision( - toolCallId: string, - timeoutMs: number, - abortSignal?: AbortSignal -): Promise<{ status: string; message?: string } | null> { - const decision = await waitForToolConfirmation(toolCallId, timeoutMs, abortSignal) - if (!decision) return null - return { status: decision.status, message: decision.message } -} - /** * Wait for a tool completion signal (success/error/rejected) from the client. - * Unlike waitForToolDecision which returns on any status, this ignores the - * initial 'accepted' status and only returns on terminal statuses: + * Ignores intermediate statuses like `accepted` and only returns terminal statuses: * - success: client finished executing successfully * - error: client execution failed * - rejected: user clicked Skip (subagent run tools where user hasn't auto-allowed) @@ -788,13 +777,22 @@ export async function waitForToolCompletion( timeoutMs: number, abortSignal?: AbortSignal ): Promise<{ status: string; message?: string; data?: Record } | null> { - const decision = await waitForToolConfirmation(toolCallId, timeoutMs, abortSignal) + const decision = await waitForToolConfirmation(toolCallId, timeoutMs, abortSignal, { + acceptStatus: (status) => + status === 'success' || + status === 'error' || + status === 'rejected' || + status === 'background' || + status === 'cancelled' || + status === 'delivered', + }) if ( decision?.status === 'success' || decision?.status === 'error' || decision?.status === 'rejected' || decision?.status === 'background' || - decision?.status === 'cancelled' + decision?.status === 'cancelled' || + decision?.status === 'delivered' ) { return decision } diff --git a/packages/db/migrations/0180_amused_marvel_boy.sql b/packages/db/migrations/0180_amused_marvel_boy.sql new file mode 100644 index 00000000000..a95ca184f6e --- /dev/null +++ b/packages/db/migrations/0180_amused_marvel_boy.sql @@ -0,0 +1,6 @@ +ALTER TABLE "copilot_async_tool_calls" ALTER COLUMN "status" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "copilot_async_tool_calls" ALTER COLUMN "status" SET DEFAULT 'pending'::text;--> statement-breakpoint +DROP TYPE "public"."copilot_async_tool_status";--> statement-breakpoint +CREATE TYPE "public"."copilot_async_tool_status" AS ENUM('pending', 'running', 'completed', 'failed', 'cancelled', 'delivered');--> statement-breakpoint +ALTER TABLE "copilot_async_tool_calls" ALTER COLUMN "status" SET DEFAULT 'pending'::"public"."copilot_async_tool_status";--> statement-breakpoint +ALTER TABLE "copilot_async_tool_calls" ALTER COLUMN "status" SET DATA TYPE "public"."copilot_async_tool_status" USING "status"::"public"."copilot_async_tool_status"; \ No newline at end of file diff --git a/packages/db/migrations/meta/0180_snapshot.json b/packages/db/migrations/meta/0180_snapshot.json new file mode 100644 index 00000000000..7bf1b4024a8 --- /dev/null +++ b/packages/db/migrations/meta/0180_snapshot.json @@ -0,0 +1,14430 @@ +{ + "id": "a5764b4b-e29a-4c50-a489-db8524d8101b", + "prevId": "4809b655-6605-4912-90ea-bb9fe1d1fe4d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "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": 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 + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_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": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "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": {} + } + }, + "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": {} + } + }, + "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_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.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_idx": { + "name": "chat_archived_at_idx", + "columns": [ + { + "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 + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "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 + }, + "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": {} + } + }, + "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_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_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 + }, + "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.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 + }, + "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_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_archived_at_idx": { + "name": "doc_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_idx": { + "name": "doc_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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": "cascade", + "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.form": { + "name": "form", + "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": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"form\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_archived_at_idx": { + "name": "form_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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 + }, + "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": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "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()" + } + }, + "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": {} + } + }, + "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": {}, + "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_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.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "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_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": "no action", + "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_idx": { + "name": "kc_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_idx": { + "name": "kc_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_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 + }, + "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_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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": {} + } + }, + "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.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "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 + } + }, + "indexes": { + "oauth_access_token_access_token_idx": { + "name": "oauth_access_token_access_token_idx", + "columns": [ + { + "expression": "access_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_token_idx": { + "name": "oauth_access_token_refresh_token_idx", + "columns": [ + { + "expression": "refresh_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_application_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_access_token_unique": { + "name": "oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": ["access_token"] + }, + "oauth_access_token_refresh_token_unique": { + "name": "oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": ["refresh_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_application": { + "name": "oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_urls": { + "name": "redirect_urls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "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": { + "oauth_application_client_id_idx": { + "name": "oauth_application_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_application_user_id_user_id_fk": { + "name": "oauth_application_user_id_user_id_fk", + "tableFrom": "oauth_application", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_application_client_id_unique": { + "name": "oauth_application_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "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 + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_user_client_idx": { + "name": "oauth_consent_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_application_client_id_fk": { + "name": "oauth_consent_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_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 + }, + "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 + }, + "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.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 + } + }, + "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": {} + } + }, + "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()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "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_org_name_unique": { + "name": "permission_group_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": {} + }, + "permission_group_org_auto_add_unique": { + "name": "permission_group_org_auto_add_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = 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 + }, + "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_user_id_unique": { + "name": "permission_group_member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "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_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.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.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 + }, + "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 + }, + "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": "'[]'" + }, + "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.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 + }, + "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 + }, + "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.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "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": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_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 + }, + "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": {} + } + }, + "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": {}, + "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_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "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'" + }, + "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": "integer", + "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 + } + }, + "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 + }, + "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": {} + } + }, + "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 + }, + "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_table_id_idx": { + "name": "user_table_rows_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_data_gin_idx": { + "name": "user_table_rows_data_gin_idx", + "columns": [ + { + "expression": "data", + "isExpression": false, + "asc": 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": {} + } + }, + "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": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_idx": { + "name": "webhook_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "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 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "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 + }, + "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": {} + } + }, + "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 + }, + "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_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 + }, + "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()" + } + }, + "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": {} + } + }, + "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": {} + } + }, + "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": "'{}'" + }, + "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_idx": { + "name": "workflow_mcp_tool_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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 + }, + "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 + }, + "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_idx": { + "name": "workflow_schedule_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "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": { + "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_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" + } + }, + "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 + }, + "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_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_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": {} + } + }, + "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_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 + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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()" + } + }, + "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_name_active_unique": { + "name": "workspace_files_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": 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_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_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": {} + } + }, + "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_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_invitation": { + "name": "workspace_invitation", + "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 + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "workspace_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "org_invitation_id": { + "name": "org_invitation_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "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_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "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"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": ["workflow", "wand", "copilot", "workspace-chat", "mcp_copilot", "mothership_block"] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 9111427f4aa..76a5e9858ac 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1254,6 +1254,13 @@ "when": 1774139366798, "tag": "0179_burly_luckman", "breakpoints": true + }, + { + "idx": 180, + "version": "7", + "when": 1774305252055, + "tag": "0180_amused_marvel_boy", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index d54caf63365..f3f1e169ac2 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1667,8 +1667,7 @@ export const copilotAsyncToolStatusEnum = pgEnum('copilot_async_tool_status', [ 'completed', 'failed', 'cancelled', - 'resume_enqueued', - 'resumed', + 'delivered', ]) export type CopilotRunStatus = (typeof copilotRunStatusEnum.enumValues)[number] From df7e635e5287737f9c7a202992e4487160897346 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 16:22:06 -0700 Subject: [PATCH 24/31] Fixes --- .../resource-content/resource-content.tsx | 6 +- .../[workspaceId]/home/hooks/use-chat.ts | 6 +- .../utils/workflow-execution-utils.ts | 2 + .../client-sse/run-tool-execution.test.ts | 88 ++++++++ .../copilot/client-sse/run-tool-execution.ts | 32 ++- .../lib/copilot/orchestrator/index.test.ts | 2 +- apps/sim/lib/copilot/orchestrator/index.ts | 194 +++++++++--------- 7 files changed, 223 insertions(+), 107 deletions(-) create mode 100644 apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts 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 9801e89b356..ac99366d48b 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 @@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation' import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn' import { Download, FileX, SquareArrowUpRight, WorkflowX } from '@/components/emcn/icons' import { + cancelRunToolExecution, markRunToolManuallyStopped, reportManualRunToolStop, } from '@/lib/copilot/client-sse/run-tool-execution' @@ -191,9 +192,10 @@ export function EmbeddedWorkflowActions({ workspaceId, workflowId }: EmbeddedWor setActiveWorkflow(workflowId) if (isExecuting) { - markRunToolManuallyStopped(workflowId) + const toolCallId = markRunToolManuallyStopped(workflowId) + cancelRunToolExecution(workflowId) await handleCancelExecution() - await reportManualRunToolStop(workflowId) + await reportManualRunToolStop(workflowId, toolCallId) return } 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 7ee0d093bf9..f5900149e3b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' import { usePathname } from 'next/navigation' import { + cancelRunToolExecution, executeRunToolOnClient, markRunToolManuallyStopped, reportManualRunToolStop, @@ -1433,7 +1434,8 @@ export function useChat( for (const [workflowId, wfExec] of execState.workflowExecutions) { if (!wfExec.isExecuting) continue - markRunToolManuallyStopped(workflowId) + const toolCallId = markRunToolManuallyStopped(workflowId) + cancelRunToolExecution(workflowId) const executionId = execState.getCurrentExecutionId(workflowId) if (executionId) { @@ -1466,7 +1468,7 @@ export function useChat( execState.setIsDebugging(workflowId, false) execState.setActiveBlocks(workflowId, new Set()) - reportManualRunToolStop(workflowId).catch(() => {}) + reportManualRunToolStop(workflowId, toolCallId).catch(() => {}) } }, [invalidateChatQueries, persistPartialResponse, executionStream]) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts index 32d286ddf56..c63345a1c88 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts @@ -570,6 +570,7 @@ export interface WorkflowExecutionOptions { onBlockComplete?: (blockId: string, output: any) => Promise overrideTriggerType?: 'chat' | 'manual' | 'api' | 'copilot' stopAfterBlockId?: string + abortSignal?: AbortSignal /** For run_from_block / run_block: start from a specific block using cached state */ runFromBlock?: { startBlockId: string @@ -643,6 +644,7 @@ export async function executeWorkflowWithFullLogging( 'Content-Type': 'application/json', }, body: JSON.stringify(payload), + signal: options.abortSignal, }) if (!response.ok) { diff --git a/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts b/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts new file mode 100644 index 00000000000..ed0e1598333 --- /dev/null +++ b/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts @@ -0,0 +1,88 @@ +/** + * @vitest-environment jsdom + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { executeWorkflowWithFullLogging } = vi.hoisted(() => ({ + executeWorkflowWithFullLogging: vi.fn(), +})) + +const setIsExecuting = vi.fn() +const setCurrentExecutionId = vi.fn() +const getWorkflowExecution = vi.fn(() => ({ isExecuting: false })) + +vi.mock( + '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils', + () => ({ + executeWorkflowWithFullLogging, + }) +) + +vi.mock('@/stores/execution/store', () => ({ + useExecutionStore: { + getState: () => ({ + getWorkflowExecution, + setIsExecuting, + setCurrentExecutionId, + }), + }, +})) + +vi.mock('@/stores/workflows/registry/store', () => ({ + useWorkflowRegistry: { + getState: () => ({ + activeWorkflowId: 'wf-1', + setActiveWorkflow: vi.fn(), + }), + }, +})) + +import { + cancelRunToolExecution, + executeRunToolOnClient, + reportManualRunToolStop, +} from './run-tool-execution' + +describe('run tool execution cancellation', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('passes an abort signal into executeWorkflowWithFullLogging and aborts it', async () => { + let capturedSignal: AbortSignal | undefined + executeWorkflowWithFullLogging.mockImplementationOnce(async (options: any) => { + capturedSignal = options.abortSignal + await new Promise((_, reject) => { + options.abortSignal.addEventListener( + 'abort', + () => reject(new DOMException('Aborted', 'AbortError')), + { once: true } + ) + }) + }) + + executeRunToolOnClient('tool-1', 'run_workflow', { workflowId: 'wf-1' }) + await Promise.resolve() + + cancelRunToolExecution('wf-1') + await Promise.resolve() + + expect(capturedSignal?.aborted).toBe(true) + }) + + it('can report a manual stop using the explicit toolCallId override', async () => { + const fetchMock = vi.fn().mockResolvedValue({ ok: true }) + vi.stubGlobal('fetch', fetchMock) + + await reportManualRunToolStop('wf-1', 'tool-override') + + expect(fetchMock).toHaveBeenCalledWith( + '/api/copilot/confirm', + expect.objectContaining({ + method: 'POST', + body: expect.stringContaining('"toolCallId":"tool-override"'), + }) + ) + }) +}) diff --git a/apps/sim/lib/copilot/client-sse/run-tool-execution.ts b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts index 68e50237df9..2d28d149beb 100644 --- a/apps/sim/lib/copilot/client-sse/run-tool-execution.ts +++ b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts @@ -8,6 +8,7 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store' const logger = createLogger('CopilotRunToolExecution') const activeRunToolByWorkflowId = new Map() +const activeRunAbortByWorkflowId = new Map() const manuallyStoppedToolCallIds = new Set() /** @@ -40,11 +41,19 @@ export function executeRunToolOnClient( * concurrent doExecuteRunTool catch/success paths see the marker and skip * their own completion report. */ -export function markRunToolManuallyStopped(workflowId: string): void { +export function markRunToolManuallyStopped(workflowId: string): string | null { const toolCallId = activeRunToolByWorkflowId.get(workflowId) - if (!toolCallId) return + if (!toolCallId) return null manuallyStoppedToolCallIds.add(toolCallId) setToolState(toolCallId, ClientToolCallState.cancelled) + return toolCallId +} + +export function cancelRunToolExecution(workflowId: string): void { + const controller = activeRunAbortByWorkflowId.get(workflowId) + if (!controller) return + controller.abort() + activeRunAbortByWorkflowId.delete(workflowId) } /** @@ -52,8 +61,11 @@ export function markRunToolManuallyStopped(workflowId: string): void { * This lets Copilot know the run was intentionally cancelled by the user. * Call markRunToolManuallyStopped first to prevent race conditions. */ -export async function reportManualRunToolStop(workflowId: string): Promise { - const toolCallId = activeRunToolByWorkflowId.get(workflowId) +export async function reportManualRunToolStop( + workflowId: string, + toolCallIdOverride?: string | null +): Promise { + const toolCallId = toolCallIdOverride || activeRunToolByWorkflowId.get(workflowId) if (!toolCallId) return if (!manuallyStoppedToolCallIds.has(toolCallId)) { @@ -133,6 +145,8 @@ async function doExecuteRunTool( })() const { setCurrentExecutionId } = useExecutionStore.getState() + const abortController = new AbortController() + activeRunAbortByWorkflowId.set(targetWorkflowId, abortController) setIsExecuting(targetWorkflowId, true) const executionId = uuidv4() @@ -157,6 +171,7 @@ async function doExecuteRunTool( overrideTriggerType: 'copilot', stopAfterBlockId, runFromBlock, + abortSignal: abortController.signal, }) // Determine success (same logic as staging's RunWorkflowClientTool) @@ -220,6 +235,10 @@ async function doExecuteRunTool( if (activeToolCallId === toolCallId) { activeRunToolByWorkflowId.delete(targetWorkflowId) } + const activeAbortController = activeRunAbortByWorkflowId.get(targetWorkflowId) + if (activeAbortController === abortController) { + activeRunAbortByWorkflowId.delete(targetWorkflowId) + } const { setCurrentExecutionId: clearExecId } = useExecutionStore.getState() clearExecId(targetWorkflowId, null) setIsExecuting(targetWorkflowId, false) @@ -263,9 +282,8 @@ function buildResultData(result: unknown): Record | undefined { /** * Report tool completion to the server via the existing /api/copilot/confirm endpoint. - * This writes {status, message, data} to Redis. The server-side handler - * is polling Redis via waitForToolCompletion() and will pick this up, then fire-and-forget - * markToolComplete to the Go backend. + * This persists the durable async-tool row and wakes the server-side waiter so + * it can continue the paused Copilot run and notify Go. */ async function reportCompletion( toolCallId: string, diff --git a/apps/sim/lib/copilot/orchestrator/index.test.ts b/apps/sim/lib/copilot/orchestrator/index.test.ts index 5a1c30c6206..ac6afeb2ef6 100644 --- a/apps/sim/lib/copilot/orchestrator/index.test.ts +++ b/apps/sim/lib/copilot/orchestrator/index.test.ts @@ -223,7 +223,6 @@ describe('orchestrateCopilotStream async continuation', () => { } context.pendingToolPromises.set('tool-1', localPendingPromise) }) - .mockImplementationOnce(async () => {}) .mockImplementationOnce(async (url: string, opts: RequestInit) => { expect(url).toContain('/api/tools/resume') const body = JSON.parse(String(opts.body)) @@ -247,6 +246,7 @@ describe('orchestrateCopilotStream async continuation', () => { ) expect(result.success).toBe(true) + expect(runStreamLoop).toHaveBeenCalledTimes(2) expect(markAsyncToolDelivered).toHaveBeenCalledWith('tool-1') }) diff --git a/apps/sim/lib/copilot/orchestrator/index.ts b/apps/sim/lib/copilot/orchestrator/index.ts index 8782a2870e0..dda999c87a8 100644 --- a/apps/sim/lib/copilot/orchestrator/index.ts +++ b/apps/sim/lib/copilot/orchestrator/index.ts @@ -168,118 +168,122 @@ export async function orchestrateCopilotStream( break } - const continuation = context.awaitingAsyncContinuation + let continuation = context.awaitingAsyncContinuation if (!continuation) break - claimedToolCallIds = [] - const resumeWorkerId = continuation.runId || context.runId || context.messageId - claimedByWorkerId = resumeWorkerId - const claimableToolCallIds: string[] = [] - const localPendingPromises: Promise[] = [] - for (const toolCallId of continuation.pendingToolCallIds) { - const claimed = await claimCompletedAsyncToolCall(toolCallId, resumeWorkerId).catch( - () => null - ) - if (claimed) { - claimableToolCallIds.push(toolCallId) - claimedToolCallIds.push(toolCallId) - continue - } - const durableRow = await getAsyncToolCall(toolCallId).catch(() => null) - const localPendingPromise = context.pendingToolPromises.get(toolCallId) - if (!durableRow && localPendingPromise) { - claimableToolCallIds.push(toolCallId) - continue - } - if (durableRow && durableRow.status === ASYNC_TOOL_STATUS.running && localPendingPromise) { - localPendingPromises.push(localPendingPromise) - logger.info('Waiting for local async tool completion before retrying resume claim', { + for (;;) { + claimedToolCallIds = [] + const resumeWorkerId = continuation.runId || context.runId || context.messageId + claimedByWorkerId = resumeWorkerId + const claimableToolCallIds: string[] = [] + const localPendingPromises: Promise[] = [] + for (const toolCallId of continuation.pendingToolCallIds) { + const claimed = await claimCompletedAsyncToolCall(toolCallId, resumeWorkerId).catch( + () => null + ) + if (claimed) { + claimableToolCallIds.push(toolCallId) + claimedToolCallIds.push(toolCallId) + continue + } + const durableRow = await getAsyncToolCall(toolCallId).catch(() => null) + const localPendingPromise = context.pendingToolPromises.get(toolCallId) + if (!durableRow && localPendingPromise) { + claimableToolCallIds.push(toolCallId) + continue + } + if (durableRow && durableRow.status === ASYNC_TOOL_STATUS.running && localPendingPromise) { + localPendingPromises.push(localPendingPromise) + logger.info('Waiting for local async tool completion before retrying resume claim', { + toolCallId, + runId: continuation.runId, + }) + continue + } + logger.warn('Skipping already-claimed or missing async tool resume', { toolCallId, runId: continuation.runId, }) - continue } - logger.warn('Skipping already-claimed or missing async tool resume', { - toolCallId, - runId: continuation.runId, - }) - } - if (claimableToolCallIds.length === 0) { - if (localPendingPromises.length > 0) { - await Promise.allSettled(localPendingPromises) - continue + if (claimableToolCallIds.length === 0) { + if (localPendingPromises.length > 0) { + await Promise.allSettled(localPendingPromises) + continue + } + logger.warn('Skipping async resume because no tool calls were claimable', { + checkpointId: continuation.checkpointId, + runId: continuation.runId, + }) + context.awaitingAsyncContinuation = undefined + break } - logger.warn('Skipping async resume because no tool calls were claimable', { + + logger.info('Resuming async tool continuation', { checkpointId: continuation.checkpointId, runId: continuation.runId, + toolCallIds: claimableToolCallIds, }) - context.awaitingAsyncContinuation = undefined - break - } - - logger.info('Resuming async tool continuation', { - checkpointId: continuation.checkpointId, - runId: continuation.runId, - toolCallIds: claimableToolCallIds, - }) - const durableRows = await getAsyncToolCalls(claimableToolCallIds).catch(() => []) - const durableByToolCallId = new Map(durableRows.map((row) => [row.toolCallId, row])) + const durableRows = await getAsyncToolCalls(claimableToolCallIds).catch(() => []) + const durableByToolCallId = new Map(durableRows.map((row) => [row.toolCallId, row])) - const results = await Promise.all( - claimableToolCallIds.map(async (toolCallId) => { - const completion = await context.pendingToolPromises.get(toolCallId) - const toolState = context.toolCalls.get(toolCallId) - - const durable = durableByToolCallId.get(toolCallId) - const durableStatus = durable?.status - const durableResult = - durable?.result && typeof durable.result === 'object' - ? (durable.result as Record) - : undefined - const success = didAsyncToolSucceed({ - durableStatus, - durableResult, - durableError: durable?.error, - completion, - toolStateSuccess: toolState?.result?.success, - }) - const data = - durableResult || - completion?.data || - (toolState?.result?.output as Record | undefined) || - (success - ? { message: completion?.message || 'Tool completed' } - : { - error: completion?.message || durable?.error || toolState?.error || 'Tool failed', - }) + const results = await Promise.all( + claimableToolCallIds.map(async (toolCallId) => { + const completion = await context.pendingToolPromises.get(toolCallId) + const toolState = context.toolCalls.get(toolCallId) - if ( - durableStatus && - !isTerminalAsyncStatus(durableStatus) && - !isDeliveredAsyncStatus(durableStatus) - ) { - logger.warn('Async tool row was claimed for resume without terminal durable state', { - toolCallId, - status: durableStatus, + const durable = durableByToolCallId.get(toolCallId) + const durableStatus = durable?.status + const durableResult = + durable?.result && typeof durable.result === 'object' + ? (durable.result as Record) + : undefined + const success = didAsyncToolSucceed({ + durableStatus, + durableResult, + durableError: durable?.error, + completion, + toolStateSuccess: toolState?.result?.success, }) - } + const data = + durableResult || + completion?.data || + (toolState?.result?.output as Record | undefined) || + (success + ? { message: completion?.message || 'Tool completed' } + : { + error: + completion?.message || durable?.error || toolState?.error || 'Tool failed', + }) - return { - callId: toolCallId, - name: durable?.toolName || toolState?.name || '', - data, - success, - } - }) - ) + if ( + durableStatus && + !isTerminalAsyncStatus(durableStatus) && + !isDeliveredAsyncStatus(durableStatus) + ) { + logger.warn('Async tool row was claimed for resume without terminal durable state', { + toolCallId, + status: durableStatus, + }) + } + + return { + callId: toolCallId, + name: durable?.toolName || toolState?.name || '', + data, + success, + } + }) + ) - context.awaitingAsyncContinuation = undefined - route = '/api/tools/resume' - payload = { - checkpointId: continuation.checkpointId, - results, + context.awaitingAsyncContinuation = undefined + route = '/api/tools/resume' + payload = { + checkpointId: continuation.checkpointId, + results, + } + break } } From 39ff907e4c1e465ecb678c203e0a0d43e9408dc8 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 16:38:18 -0700 Subject: [PATCH 25/31] Fixes --- .../client-sse/run-tool-execution.test.ts | 9 +++---- apps/sim/lib/copilot/orchestrator/index.ts | 21 +++++++++++++-- .../orchestrator/sse/handlers/handlers.ts | 26 +++++++++++++++++++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts b/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts index ed0e1598333..00c306ac235 100644 --- a/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts +++ b/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts @@ -12,12 +12,9 @@ const setIsExecuting = vi.fn() const setCurrentExecutionId = vi.fn() const getWorkflowExecution = vi.fn(() => ({ isExecuting: false })) -vi.mock( - '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils', - () => ({ - executeWorkflowWithFullLogging, - }) -) +vi.mock('@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils', () => ({ + executeWorkflowWithFullLogging, +})) vi.mock('@/stores/execution/store', () => ({ useExecutionStore: { diff --git a/apps/sim/lib/copilot/orchestrator/index.ts b/apps/sim/lib/copilot/orchestrator/index.ts index dda999c87a8..28d3f268e29 100644 --- a/apps/sim/lib/copilot/orchestrator/index.ts +++ b/apps/sim/lib/copilot/orchestrator/index.ts @@ -164,13 +164,21 @@ export async function orchestrateCopilotStream( } if (options.abortSignal?.aborted || context.wasAborted) { + for (const [toolCallId, toolCall] of context.toolCalls) { + if (toolCall.status === 'pending' || toolCall.status === 'executing') { + toolCall.status = 'cancelled' + toolCall.endTime = Date.now() + toolCall.error = 'Stopped by user' + } + } context.awaitingAsyncContinuation = undefined break } - let continuation = context.awaitingAsyncContinuation + const continuation = context.awaitingAsyncContinuation if (!continuation) break + let resumeReady = false for (;;) { claimedToolCallIds = [] const resumeWorkerId = continuation.runId || context.runId || context.messageId @@ -192,7 +200,11 @@ export async function orchestrateCopilotStream( claimableToolCallIds.push(toolCallId) continue } - if (durableRow && durableRow.status === ASYNC_TOOL_STATUS.running && localPendingPromise) { + if ( + durableRow && + durableRow.status === ASYNC_TOOL_STATUS.running && + localPendingPromise + ) { localPendingPromises.push(localPendingPromise) logger.info('Waiting for local async tool completion before retrying resume claim', { toolCallId, @@ -283,6 +295,11 @@ export async function orchestrateCopilotStream( checkpointId: continuation.checkpointId, results, } + resumeReady = true + break + } + + if (!resumeReady) { break } } diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts index a9ad474db14..acf72d5f936 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts @@ -403,6 +403,19 @@ export const sseHandlers: Record = { } } else { toolCall.status = 'executing' + await upsertAsyncToolCall({ + runId: context.runId || crypto.randomUUID(), + toolCallId, + toolName, + args, + status: 'running', + }).catch((err) => { + logger.warn('Failed to persist async tool row for client-executable tool', { + toolCallId, + toolName, + error: err instanceof Error ? err.message : String(err), + }) + }) const completion = await waitForToolCompletion( toolCallId, options.timeout || STREAM_TIMEOUT_MS, @@ -624,6 +637,19 @@ export const subAgentHandlers: Record = { } } else { toolCall.status = 'executing' + await upsertAsyncToolCall({ + runId: context.runId || crypto.randomUUID(), + toolCallId, + toolName, + args, + status: 'running', + }).catch((err) => { + logger.warn('Failed to persist async tool row for client-executable subagent tool', { + toolCallId, + toolName, + error: err instanceof Error ? err.message : String(err), + }) + }) const completion = await waitForToolCompletion( toolCallId, options.timeout || STREAM_TIMEOUT_MS, From 7f83a0c73c3277d5fbee685366b9aeefe1aaf92b Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 17:08:48 -0700 Subject: [PATCH 26/31] Fix subaget aborts --- apps/sim/app/api/copilot/chat/route.ts | 25 +++++++++++++------ apps/sim/app/api/mothership/chat/route.ts | 25 +++++++++++++------ .../lib/copilot/orchestrator/stream/core.ts | 1 + 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 296c28a8eb8..546344dab40 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -364,16 +364,27 @@ export async function POST(req: NextRequest) { const stored: Record = { type: block.type } if (block.content) stored.content = block.content if (block.type === 'tool_call' && block.toolCall) { + const state = + block.toolCall.result?.success !== undefined + ? block.toolCall.result.success + ? 'success' + : 'error' + : block.toolCall.status + const isSubagentTool = !!block.calledBy + const isNonTerminal = + state === 'cancelled' || state === 'pending' || state === 'executing' stored.toolCall = { id: block.toolCall.id, name: block.toolCall.name, - state: - block.toolCall.result?.success !== undefined - ? block.toolCall.result.success - ? 'success' - : 'error' - : block.toolCall.status, - result: block.toolCall.result, + state, + ...(isSubagentTool && isNonTerminal + ? {} + : { result: block.toolCall.result }), + ...(isSubagentTool && isNonTerminal + ? {} + : block.toolCall.params + ? { params: block.toolCall.params } + : {}), ...(block.calledBy ? { calledBy: block.calledBy } : {}), } } diff --git a/apps/sim/app/api/mothership/chat/route.ts b/apps/sim/app/api/mothership/chat/route.ts index a399820d645..db78e45f692 100644 --- a/apps/sim/app/api/mothership/chat/route.ts +++ b/apps/sim/app/api/mothership/chat/route.ts @@ -298,16 +298,27 @@ export async function POST(req: NextRequest) { const stored: Record = { type: block.type } if (block.content) stored.content = block.content if (block.type === 'tool_call' && block.toolCall) { + const state = + block.toolCall.result?.success !== undefined + ? block.toolCall.result.success + ? 'success' + : 'error' + : block.toolCall.status + const isSubagentTool = !!block.calledBy + const isNonTerminal = + state === 'cancelled' || state === 'pending' || state === 'executing' stored.toolCall = { id: block.toolCall.id, name: block.toolCall.name, - state: - block.toolCall.result?.success !== undefined - ? block.toolCall.result.success - ? 'success' - : 'error' - : block.toolCall.status, - result: block.toolCall.result, + state, + ...(isSubagentTool && isNonTerminal + ? {} + : { result: block.toolCall.result }), + ...(isSubagentTool && isNonTerminal + ? {} + : block.toolCall.params + ? { params: block.toolCall.params } + : {}), ...(block.calledBy ? { calledBy: block.calledBy } : {}), } } diff --git a/apps/sim/lib/copilot/orchestrator/stream/core.ts b/apps/sim/lib/copilot/orchestrator/stream/core.ts index 96d85b61061..9367a3b181a 100644 --- a/apps/sim/lib/copilot/orchestrator/stream/core.ts +++ b/apps/sim/lib/copilot/orchestrator/stream/core.ts @@ -247,6 +247,7 @@ export function buildToolCallSummaries(context: StreamingContext): ToolCallSumma } else if ((status === 'pending' || status === 'executing') && toolCall.error) { status = 'error' } + return { id: toolCall.id, name: toolCall.name, From 772929c78e3f0ee74d2a85cfb4508fe4855187bb Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 17:17:15 -0700 Subject: [PATCH 27/31] Fix diff --- apps/sim/app/api/copilot/chat/route.ts | 4 +--- apps/sim/app/api/mothership/chat/route.ts | 4 +--- .../w/[workflowId]/components/panel/panel.tsx | 4 ++++ apps/sim/lib/copilot/client-sse/handlers.ts | 5 +++++ apps/sim/stores/workflow-diff/store.ts | 13 ++++++++++++- apps/sim/stores/workflow-diff/types.ts | 6 ++++++ 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 546344dab40..015975e2833 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -377,9 +377,7 @@ export async function POST(req: NextRequest) { id: block.toolCall.id, name: block.toolCall.name, state, - ...(isSubagentTool && isNonTerminal - ? {} - : { result: block.toolCall.result }), + ...(isSubagentTool && isNonTerminal ? {} : { result: block.toolCall.result }), ...(isSubagentTool && isNonTerminal ? {} : block.toolCall.params diff --git a/apps/sim/app/api/mothership/chat/route.ts b/apps/sim/app/api/mothership/chat/route.ts index db78e45f692..5e93baeff58 100644 --- a/apps/sim/app/api/mothership/chat/route.ts +++ b/apps/sim/app/api/mothership/chat/route.ts @@ -311,9 +311,7 @@ export async function POST(req: NextRequest) { id: block.toolCall.id, name: block.toolCall.name, state, - ...(isSubagentTool && isNonTerminal - ? {} - : { result: block.toolCall.result }), + ...(isSubagentTool && isNonTerminal ? {} : { result: block.toolCall.result }), ...(isSubagentTool && isNonTerminal ? {} : block.toolCall.params diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index dcd89f2edb9..9b169504d3a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -72,6 +72,7 @@ import type { ChatContext, PanelTab } from '@/stores/panel' import { usePanelStore, useVariablesStore as usePanelVariablesStore } from '@/stores/panel' import { useVariablesStore } from '@/stores/variables/store' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' +import { captureBaselineSnapshot } from '@/stores/workflow-diff/utils' import { getWorkflowWithValues } from '@/stores/workflows' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -297,6 +298,8 @@ export const Panel = memo(function Panel() { const workflowId = activeWorkflowId || useWorkflowRegistry.getState().activeWorkflowId if (!workflowId) return + const baselineWorkflow = captureBaselineSnapshot(workflowId) + fetch(`/api/workflows/${workflowId}/state`) .then((res) => { if (!res.ok) throw new Error(`State fetch failed: ${res.status}`) @@ -305,6 +308,7 @@ export const Panel = memo(function Panel() { .then((freshState) => { const diffStore = useWorkflowDiffStore.getState() return diffStore.setProposedChanges(freshState as WorkflowState, undefined, { + baselineWorkflow, skipPersist: true, }) }) diff --git a/apps/sim/lib/copilot/client-sse/handlers.ts b/apps/sim/lib/copilot/client-sse/handlers.ts index 86e5218587f..2bb6bc3e318 100644 --- a/apps/sim/lib/copilot/client-sse/handlers.ts +++ b/apps/sim/lib/copilot/client-sse/handlers.ts @@ -18,6 +18,7 @@ import type { CopilotStore, CopilotStreamInfo, CopilotToolCall } from '@/stores/ import { useVariablesStore } from '@/stores/panel/variables/store' import { useEnvironmentStore } from '@/stores/settings/environment/store' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' +import { captureBaselineSnapshot } from '@/stores/workflow-diff/utils' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import type { WorkflowState } from '@/stores/workflows/workflow/types' import { executeRunToolOnClient } from './run-tool-execution' @@ -616,6 +617,8 @@ export const sseHandlers: Record = { if (!workflowId) { logger.warn('[SSE] edit_workflow result has no workflowId, skipping diff') } else { + const baselineWorkflow = captureBaselineSnapshot(workflowId) + // Re-fetch the state the server just wrote to DB. // Never use the response's workflowState directly — that would // mean client and server independently track state, creating @@ -629,6 +632,7 @@ export const sseHandlers: Record = { .then((freshState) => { const diffStore = useWorkflowDiffStore.getState() return diffStore.setProposedChanges(freshState as WorkflowState, undefined, { + baselineWorkflow, skipPersist: true, }) }) @@ -642,6 +646,7 @@ export const sseHandlers: Record = { const diffStore = useWorkflowDiffStore.getState() diffStore .setProposedChanges(resultPayload.workflowState as WorkflowState, undefined, { + baselineWorkflow, skipPersist: true, }) .catch(() => {}) diff --git a/apps/sim/stores/workflow-diff/store.ts b/apps/sim/stores/workflow-diff/store.ts index 49c19990cb3..341d85c191c 100644 --- a/apps/sim/stores/workflow-diff/store.ts +++ b/apps/sim/stores/workflow-diff/store.ts @@ -88,7 +88,18 @@ export const useWorkflowDiffStore = create Date: Mon, 23 Mar 2026 17:26:31 -0700 Subject: [PATCH 28/31] Add delegating state to subagents --- .../components/agent-group/agent-group.tsx | 33 ++++++--- .../message-content/message-content.tsx | 73 ++++++++++++------- 2 files changed, 70 insertions(+), 36 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index 56c7b979889..ac024b84c81 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { ChevronDown } from '@/components/emcn' +import { ChevronDown, PillsRing } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import type { ToolCallData } from '../../../../types' import { getAgentIcon } from '../../utils' @@ -15,31 +15,34 @@ interface AgentGroupProps { agentName: string agentLabel: string items: AgentGroupItem[] + isDelegating?: boolean autoCollapse?: boolean + defaultExpanded?: boolean } const FADE_MS = 300 +function isToolDone(status: ToolCallData['status']): boolean { + return status === 'success' || status === 'error' || status === 'cancelled' +} + export function AgentGroup({ agentName, agentLabel, items, + isDelegating = false, autoCollapse = false, + defaultExpanded = false, }: AgentGroupProps) { const AgentIcon = getAgentIcon(agentName) const hasItems = items.length > 0 const toolItems = items.filter( (item): item is Extract => item.type === 'tool' ) - const allDone = - toolItems.length > 0 && - toolItems.every( - (t) => - t.data.status === 'success' || t.data.status === 'error' || t.data.status === 'cancelled' - ) + const allDone = toolItems.length > 0 && toolItems.every((t) => isToolDone(t.data.status)) - const [expanded, setExpanded] = useState(!allDone) - const [mounted, setMounted] = useState(!allDone) + const [expanded, setExpanded] = useState(defaultExpanded || !allDone) + const [mounted, setMounted] = useState(defaultExpanded || !allDone) const didAutoCollapseRef = useRef(allDone) useEffect(() => { @@ -66,7 +69,11 @@ export function AgentGroup({ className='flex cursor-pointer items-center gap-[8px]' >
- + {isDelegating ? ( + + ) : ( + + )}
{agentLabel}
- + {isDelegating ? ( + + ) : ( + + )}
{agentLabel} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index afe1bd7819d..f3f786de437 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -18,6 +18,8 @@ interface AgentGroupSegment { agentName: string agentLabel: string items: AgentGroupItem[] + isDelegating: boolean + isOpen: boolean } interface OptionsSegment { @@ -55,6 +57,14 @@ function resolveAgentLabel(key: string): string { return SUBAGENT_LABELS[key as SubagentName] ?? formatToolName(key) } +function isToolDone(status: ToolCallData['status']): boolean { + return status === 'success' || status === 'error' || status === 'cancelled' +} + +function isDelegatingTool(tc: NonNullable): boolean { + return tc.status === 'executing' +} + function mapToolStatusToClientState( status: ContentBlock['toolCall'] extends { status: infer T } ? T : string ) { @@ -106,12 +116,16 @@ function toToolData(tc: NonNullable): ToolCallData { function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { const segments: MessageSegment[] = [] let group: AgentGroupSegment | null = null + const pushGroup = (nextGroup: AgentGroupSegment, isOpen = false) => { + segments.push({ ...nextGroup, isOpen }) + } for (let i = 0; i < blocks.length; i++) { const block = blocks[i] if (block.type === 'subagent_text') { if (!block.content || !group) continue + group.isDelegating = false const lastItem = group.items[group.items.length - 1] if (lastItem?.type === 'text') { lastItem.content += block.content @@ -125,6 +139,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { if (!block.content?.trim()) continue if (block.subagent) { if (group && group.agentName === block.subagent) { + group.isDelegating = false const lastItem = group.items[group.items.length - 1] if (lastItem?.type === 'text') { lastItem.content += block.content @@ -135,7 +150,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { } } if (group) { - segments.push(group) + pushGroup(group) group = null } const last = segments[segments.length - 1] @@ -153,17 +168,19 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { if (group && group.agentName === key) continue const dispatchToolName = SUBAGENT_DISPATCH_TOOLS[key] + let inheritedDelegation = false if (group && dispatchToolName) { const last = group.items[group.items.length - 1] if (last?.type === 'tool' && last.data.toolName === dispatchToolName) { + inheritedDelegation = !isToolDone(last.data.status) && Boolean(last.data.streamingArgs) group.items.pop() } if (group.items.length > 0) { - segments.push(group) + pushGroup(group) } group = null } else if (group) { - segments.push(group) + pushGroup(group) group = null } @@ -173,6 +190,8 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { agentName: key, agentLabel: resolveAgentLabel(key), items: [], + isDelegating: inheritedDelegation, + isOpen: false, } continue } @@ -186,7 +205,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { if (isDispatch) { if (!group || group.agentName !== tc.name) { if (group) { - segments.push(group) + pushGroup(group) group = null } group = { @@ -195,18 +214,22 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { agentName: tc.name, agentLabel: resolveAgentLabel(tc.name), items: [], + isDelegating: false, + isOpen: false, } } + group.isDelegating = isDelegatingTool(tc) continue } const tool = toToolData(tc) if (tc.calledBy && group && group.agentName === tc.calledBy) { + group.isDelegating = false group.items.push({ type: 'tool', data: tool }) } else if (tc.calledBy) { if (group) { - segments.push(group) + pushGroup(group) group = null } group = { @@ -215,13 +238,15 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { agentName: tc.calledBy, agentLabel: resolveAgentLabel(tc.calledBy), items: [{ type: 'tool', data: tool }], + isDelegating: false, + isOpen: false, } } else { if (group && group.agentName === 'mothership') { group.items.push({ type: 'tool', data: tool }) } else { if (group) { - segments.push(group) + pushGroup(group) group = null } group = { @@ -230,6 +255,8 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { agentName: 'mothership', agentLabel: 'Mothership', items: [{ type: 'tool', data: tool }], + isDelegating: false, + isOpen: false, } } } @@ -239,7 +266,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { if (block.type === 'options') { if (!block.options?.length) continue if (group) { - segments.push(group) + pushGroup(group) group = null } segments.push({ type: 'options', items: block.options }) @@ -248,7 +275,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { if (block.type === 'subagent_end') { if (group) { - segments.push(group) + pushGroup(group) group = null } continue @@ -256,14 +283,14 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { if (block.type === 'stopped') { if (group) { - segments.push(group) + pushGroup(group) group = null } segments.push({ type: 'stopped' }) } } - if (group) segments.push(group) + if (group) pushGroup(group, true) return segments } @@ -327,19 +354,18 @@ export function MessageContent({ if (lastSegment.type === 'agent_group') { const toolItems = lastSegment.items.filter((item) => item.type === 'tool') allLastGroupToolsDone = - toolItems.length > 0 && - toolItems.every( - (t) => - t.type === 'tool' && - (t.data.status === 'success' || - t.data.status === 'error' || - t.data.status === 'cancelled') - ) + toolItems.length > 0 && toolItems.every((t) => t.type === 'tool' && isToolDone(t.data.status)) } const hasSubagentEnded = blocks.some((b) => b.type === 'subagent_end') const showTrailingThinking = isStreaming && !hasTrailingContent && (hasSubagentEnded || allLastGroupToolsDone) + const lastOpenSubagentGroupId = [...segments] + .reverse() + .find( + (segment): segment is AgentGroupSegment => + segment.type === 'agent_group' && segment.agentName !== 'mothership' && segment.isOpen + )?.id return (
@@ -358,21 +384,18 @@ export function MessageContent({ const toolItems = segment.items.filter((item) => item.type === 'tool') const allToolsDone = toolItems.length === 0 || - toolItems.every( - (t) => - t.type === 'tool' && - (t.data.status === 'success' || - t.data.status === 'error' || - t.data.status === 'cancelled') - ) + toolItems.every((t) => t.type === 'tool' && isToolDone(t.data.status)) const hasFollowingText = segments.slice(i + 1).some((s) => s.type === 'text') return (
) From a943010d82b9e285ee90cd0e4273f3e306f6f44b Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 17:57:22 -0700 Subject: [PATCH 29/31] Fix build --- .../message-content/message-content.tsx | 2 +- apps/sim/stores/workflow-diff/store.ts | 27 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index f3f786de437..6b0fa177071 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -170,7 +170,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { const dispatchToolName = SUBAGENT_DISPATCH_TOOLS[key] let inheritedDelegation = false if (group && dispatchToolName) { - const last = group.items[group.items.length - 1] + const last: AgentGroupItem | undefined = group.items[group.items.length - 1] if (last?.type === 'tool' && last.data.toolName === dispatchToolName) { inheritedDelegation = !isToolDone(last.data.status) && Boolean(last.data.streamingArgs) group.items.pop() diff --git a/apps/sim/stores/workflow-diff/store.ts b/apps/sim/stores/workflow-diff/store.ts index 341d85c191c..a901e68d0f2 100644 --- a/apps/sim/stores/workflow-diff/store.ts +++ b/apps/sim/stores/workflow-diff/store.ts @@ -150,16 +150,9 @@ export const useWorkflowDiffStore = create { + if (msgId) set({ _triggerMessageId: msgId }) + }) + } + logger.info('Workflow diff applied optimistically', { workflowId: activeWorkflowId, blocks: Object.keys(candidateState.blocks || {}).length, From 63011d992eaadbcbd69bb07f56f691ae1f5d0d15 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 18:04:48 -0700 Subject: [PATCH 30/31] Fix sandbox --- .../tools/server/files/workspace-file.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) 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 2de6f47a882..f8f0e2a3b60 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' -import PptxGenJS from 'pptxgenjs' import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' +import { generatePptxFromCode } from '@/lib/execution/pptx-vm' import type { WorkspaceFileArgs, WorkspaceFileResult } from '@/lib/copilot/tools/shared/schemas' import { deleteWorkspaceFile, @@ -25,23 +25,6 @@ const EXT_TO_MIME: Record = { '.pptx': PPTX_MIME, } -export async function generatePptxFromCode(code: string, workspaceId: string): Promise { - const pptx = new PptxGenJS() - - async function getFileBase64(fileId: string): Promise { - const record = await getWorkspaceFile(workspaceId, fileId) - if (!record) throw new Error(`File not found: ${fileId}`) - const buffer = await downloadWsFile(record) - const mime = record.type || 'image/png' - return `data:${mime};base64,${buffer.toString('base64')}` - } - - const fn = new Function('pptx', 'getFileBase64', `return (async () => { ${code} })()`) - await fn(pptx, getFileBase64) - const output = await pptx.write({ outputType: 'nodebuffer' }) - return output as Buffer -} - function inferContentType(fileName: string, explicitType?: string): string { if (explicitType) return explicitType const ext = fileName.slice(fileName.lastIndexOf('.')).toLowerCase() From 405f57c05539103c737de129d19e7d2170e021e1 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 23 Mar 2026 18:06:37 -0700 Subject: [PATCH 31/31] Fix lint --- apps/sim/lib/copilot/tools/server/files/workspace-file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f8f0e2a3b60..f9c3f49f88e 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' -import { generatePptxFromCode } from '@/lib/execution/pptx-vm' import type { WorkspaceFileArgs, WorkspaceFileResult } from '@/lib/copilot/tools/shared/schemas' +import { generatePptxFromCode } from '@/lib/execution/pptx-vm' import { deleteWorkspaceFile, downloadWorkspaceFile as downloadWsFile,