mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-03 19:24:57 -05:00
Compare commits
7 Commits
feat/timeo
...
fix/traces
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ac79b0443 | ||
|
|
47cb0a2cc9 | ||
|
|
a529db112e | ||
|
|
c69d6bf976 | ||
|
|
567f39267c | ||
|
|
12409f5eb5 | ||
|
|
7386d227eb |
@@ -213,25 +213,6 @@ Different subscription plans have different usage limits:
|
|||||||
| **Team** | $40/seat (pooled, adjustable) | 300 sync, 2,500 async |
|
| **Team** | $40/seat (pooled, adjustable) | 300 sync, 2,500 async |
|
||||||
| **Enterprise** | Custom | Custom |
|
| **Enterprise** | Custom | Custom |
|
||||||
|
|
||||||
## Execution Time Limits
|
|
||||||
|
|
||||||
Workflows have maximum execution time limits based on your subscription plan:
|
|
||||||
|
|
||||||
| Plan | Sync Execution Limit |
|
|
||||||
|------|---------------------|
|
|
||||||
| **Free** | 5 minutes |
|
|
||||||
| **Pro** | 60 minutes |
|
|
||||||
| **Team** | 60 minutes |
|
|
||||||
| **Enterprise** | 60 minutes |
|
|
||||||
|
|
||||||
**Sync executions** run immediately and return results directly. These are triggered via the API with `async: false` (default) or through the UI.
|
|
||||||
|
|
||||||
**Async executions** (triggered via API with `async: true`, webhooks, or schedules) run in the background with a 90-minute time limit for all plans.
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
If a workflow exceeds its time limit, it will be terminated and marked as failed with a timeout error. Design long-running workflows to use async execution or break them into smaller workflows.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Billing Model
|
## Billing Model
|
||||||
|
|
||||||
Sim uses a **base subscription + overage** billing model:
|
Sim uses a **base subscription + overage** billing model:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||||
import SSOForm from '@/app/(auth)/sso/sso-form'
|
import SSOForm from '@/ee/sso/components/sso-form'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Database,
|
Database,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
Timer,
|
Workflow,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
@@ -44,7 +44,7 @@ interface PricingTier {
|
|||||||
const FREE_PLAN_FEATURES: PricingFeature[] = [
|
const FREE_PLAN_FEATURES: PricingFeature[] = [
|
||||||
{ icon: DollarSign, text: '$20 usage limit' },
|
{ icon: DollarSign, text: '$20 usage limit' },
|
||||||
{ icon: HardDrive, text: '5GB file storage' },
|
{ icon: HardDrive, text: '5GB file storage' },
|
||||||
{ icon: Timer, text: '5 min execution limit' },
|
{ icon: Workflow, text: 'Public template access' },
|
||||||
{ icon: Database, text: 'Limited log retention' },
|
{ icon: Database, text: 'Limited log retention' },
|
||||||
{ icon: Code2, text: 'CLI/SDK Access' },
|
{ icon: Code2, text: 'CLI/SDK Access' },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq, lt, sql } from 'drizzle-orm'
|
import { and, eq, lt, sql } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { verifyCronAuth } from '@/lib/auth/internal'
|
import { verifyCronAuth } from '@/lib/auth/internal'
|
||||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
|
||||||
|
|
||||||
const logger = createLogger('CleanupStaleExecutions')
|
const logger = createLogger('CleanupStaleExecutions')
|
||||||
|
|
||||||
const STALE_THRESHOLD_MS = getMaxExecutionTimeout() + 5 * 60 * 1000
|
const STALE_THRESHOLD_MINUTES = 30
|
||||||
const STALE_THRESHOLD_MINUTES = Math.ceil(STALE_THRESHOLD_MS / 60000)
|
|
||||||
const MAX_INT32 = 2_147_483_647
|
const MAX_INT32 = 2_147_483_647
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { and, eq } from 'drizzle-orm'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateInternalToken } from '@/lib/auth/internal'
|
import { generateInternalToken } from '@/lib/auth/internal'
|
||||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
|
|
||||||
const logger = createLogger('WorkflowMcpServeAPI')
|
const logger = createLogger('WorkflowMcpServeAPI')
|
||||||
@@ -265,7 +264,7 @@ async function handleToolsCall(
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({ input: params.arguments || {}, triggerType: 'mcp' }),
|
body: JSON.stringify({ input: params.arguments || {}, triggerType: 'mcp' }),
|
||||||
signal: AbortSignal.timeout(getMaxExecutionTimeout()),
|
signal: AbortSignal.timeout(600000), // 10 minute timeout
|
||||||
})
|
})
|
||||||
|
|
||||||
const executeResult = await response.json()
|
const executeResult = await response.json()
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
|
|
||||||
import { getExecutionTimeout } from '@/lib/core/execution-limits'
|
|
||||||
import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types'
|
|
||||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||||
import { mcpService } from '@/lib/mcp/service'
|
import { mcpService } from '@/lib/mcp/service'
|
||||||
import type { McpTool, McpToolCall, McpToolResult } from '@/lib/mcp/types'
|
import type { McpTool, McpToolCall, McpToolResult } from '@/lib/mcp/types'
|
||||||
@@ -10,6 +7,7 @@ import {
|
|||||||
categorizeError,
|
categorizeError,
|
||||||
createMcpErrorResponse,
|
createMcpErrorResponse,
|
||||||
createMcpSuccessResponse,
|
createMcpSuccessResponse,
|
||||||
|
MCP_CONSTANTS,
|
||||||
validateStringParam,
|
validateStringParam,
|
||||||
} from '@/lib/mcp/utils'
|
} from '@/lib/mcp/utils'
|
||||||
|
|
||||||
@@ -173,16 +171,13 @@ export const POST = withMcpAuth('read')(
|
|||||||
arguments: args,
|
arguments: args,
|
||||||
}
|
}
|
||||||
|
|
||||||
const userSubscription = await getHighestPrioritySubscription(userId)
|
|
||||||
const executionTimeout = getExecutionTimeout(
|
|
||||||
userSubscription?.plan as SubscriptionPlan | undefined,
|
|
||||||
'sync'
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = await Promise.race([
|
const result = await Promise.race([
|
||||||
mcpService.executeTool(userId, serverId, toolCall, workspaceId),
|
mcpService.executeTool(userId, serverId, toolCall, workspaceId),
|
||||||
new Promise<never>((_, reject) =>
|
new Promise<never>((_, reject) =>
|
||||||
setTimeout(() => reject(new Error('Tool execution timeout')), executionTimeout)
|
setTimeout(
|
||||||
|
() => reject(new Error('Tool execution timeout')),
|
||||||
|
MCP_CONSTANTS.EXECUTION_TIMEOUT
|
||||||
|
)
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
|
|||||||
import {
|
import {
|
||||||
InvitationsNotAllowedError,
|
InvitationsNotAllowedError,
|
||||||
validateInvitationsAllowed,
|
validateInvitationsAllowed,
|
||||||
} from '@/executor/utils/permission-check'
|
} from '@/ee/access-control/utils/permission-check'
|
||||||
|
|
||||||
const logger = createLogger('OrganizationInvitations')
|
const logger = createLogger('OrganizationInvitations')
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { getTimeoutErrorMessage, isTimeoutError } from '@/lib/core/execution-limits'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { SSE_HEADERS } from '@/lib/core/utils/sse'
|
import { SSE_HEADERS } from '@/lib/core/utils/sse'
|
||||||
import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
||||||
@@ -117,16 +116,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
const loggingSession = new LoggingSession(workflowId, executionId, 'manual', requestId)
|
const loggingSession = new LoggingSession(workflowId, executionId, 'manual', requestId)
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
let isStreamClosed = false
|
let isStreamClosed = false
|
||||||
let isTimedOut = false
|
|
||||||
|
|
||||||
const syncTimeout = preprocessResult.executionTimeout?.sync
|
|
||||||
let timeoutId: NodeJS.Timeout | undefined
|
|
||||||
if (syncTimeout) {
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
isTimedOut = true
|
|
||||||
abortController.abort()
|
|
||||||
}, syncTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = new ReadableStream<Uint8Array>({
|
const stream = new ReadableStream<Uint8Array>({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
@@ -178,33 +167,13 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (result.status === 'cancelled') {
|
if (result.status === 'cancelled') {
|
||||||
if (isTimedOut && syncTimeout) {
|
sendEvent({
|
||||||
const timeoutErrorMessage = getTimeoutErrorMessage(null, syncTimeout)
|
type: 'execution:cancelled',
|
||||||
logger.info(`[${requestId}] Run-from-block execution timed out`, {
|
timestamp: new Date().toISOString(),
|
||||||
timeoutMs: syncTimeout,
|
executionId,
|
||||||
})
|
workflowId,
|
||||||
|
data: { duration: result.metadata?.duration || 0 },
|
||||||
await loggingSession.markAsFailed(timeoutErrorMessage)
|
})
|
||||||
|
|
||||||
sendEvent({
|
|
||||||
type: 'execution:error',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
executionId,
|
|
||||||
workflowId,
|
|
||||||
data: {
|
|
||||||
error: timeoutErrorMessage,
|
|
||||||
duration: result.metadata?.duration || 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
sendEvent({
|
|
||||||
type: 'execution:cancelled',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
executionId,
|
|
||||||
workflowId,
|
|
||||||
data: { duration: result.metadata?.duration || 0 },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
sendEvent({
|
sendEvent({
|
||||||
type: 'execution:completed',
|
type: 'execution:completed',
|
||||||
@@ -221,25 +190,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const isTimeout = isTimeoutError(error) || isTimedOut
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||||
const errorMessage = isTimeout
|
logger.error(`[${requestId}] Run-from-block execution failed: ${errorMessage}`)
|
||||||
? getTimeoutErrorMessage(error, syncTimeout)
|
|
||||||
: error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: 'Unknown error'
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Run-from-block execution failed: ${errorMessage}`, {
|
|
||||||
isTimeout,
|
|
||||||
})
|
|
||||||
|
|
||||||
const executionResult = hasExecutionResult(error) ? error.executionResult : undefined
|
const executionResult = hasExecutionResult(error) ? error.executionResult : undefined
|
||||||
|
|
||||||
await loggingSession.safeCompleteWithError({
|
|
||||||
totalDurationMs: executionResult?.metadata?.duration,
|
|
||||||
error: { message: errorMessage },
|
|
||||||
traceSpans: executionResult?.logs as any,
|
|
||||||
})
|
|
||||||
|
|
||||||
sendEvent({
|
sendEvent({
|
||||||
type: 'execution:error',
|
type: 'execution:error',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -251,7 +206,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
if (timeoutId) clearTimeout(timeoutId)
|
|
||||||
if (!isStreamClosed) {
|
if (!isStreamClosed) {
|
||||||
try {
|
try {
|
||||||
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'))
|
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'))
|
||||||
@@ -262,7 +216,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
isStreamClosed = true
|
isStreamClosed = true
|
||||||
if (timeoutId) clearTimeout(timeoutId)
|
|
||||||
abortController.abort()
|
abortController.abort()
|
||||||
markExecutionCancelled(executionId).catch(() => {})
|
markExecutionCancelled(executionId).catch(() => {})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { validate as uuidValidate, v4 as uuidv4 } from 'uuid'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags'
|
import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags'
|
||||||
import { getTimeoutErrorMessage, isTimeoutError } from '@/lib/core/execution-limits'
|
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { SSE_HEADERS } from '@/lib/core/utils/sse'
|
import { SSE_HEADERS } from '@/lib/core/utils/sse'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
@@ -121,6 +120,10 @@ type AsyncExecutionParams = {
|
|||||||
triggerType: CoreTriggerType
|
triggerType: CoreTriggerType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles async workflow execution by queueing a background job.
|
||||||
|
* Returns immediately with a 202 Accepted response containing the job ID.
|
||||||
|
*/
|
||||||
async function handleAsyncExecution(params: AsyncExecutionParams): Promise<NextResponse> {
|
async function handleAsyncExecution(params: AsyncExecutionParams): Promise<NextResponse> {
|
||||||
const { requestId, workflowId, userId, input, triggerType } = params
|
const { requestId, workflowId, userId, input, triggerType } = params
|
||||||
|
|
||||||
@@ -402,7 +405,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
|
|
||||||
if (!enableSSE) {
|
if (!enableSSE) {
|
||||||
logger.info(`[${requestId}] Using non-SSE execution (direct JSON response)`)
|
logger.info(`[${requestId}] Using non-SSE execution (direct JSON response)`)
|
||||||
const syncTimeout = preprocessResult.executionTimeout?.sync
|
|
||||||
try {
|
try {
|
||||||
const metadata: ExecutionMetadata = {
|
const metadata: ExecutionMetadata = {
|
||||||
requestId,
|
requestId,
|
||||||
@@ -436,7 +438,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
includeFileBase64,
|
includeFileBase64,
|
||||||
base64MaxBytes,
|
base64MaxBytes,
|
||||||
stopAfterBlockId,
|
stopAfterBlockId,
|
||||||
abortSignal: syncTimeout ? AbortSignal.timeout(syncTimeout) : undefined,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const outputWithBase64 = includeFileBase64
|
const outputWithBase64 = includeFileBase64
|
||||||
@@ -472,23 +473,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
|
|
||||||
return NextResponse.json(filteredResult)
|
return NextResponse.json(filteredResult)
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const isTimeout = isTimeoutError(error)
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||||
const errorMessage = isTimeout
|
logger.error(`[${requestId}] Non-SSE execution failed: ${errorMessage}`)
|
||||||
? getTimeoutErrorMessage(error, syncTimeout)
|
|
||||||
: error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: 'Unknown error'
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Non-SSE execution failed: ${errorMessage}`, { isTimeout })
|
|
||||||
|
|
||||||
const executionResult = hasExecutionResult(error) ? error.executionResult : undefined
|
const executionResult = hasExecutionResult(error) ? error.executionResult : undefined
|
||||||
|
|
||||||
await loggingSession.safeCompleteWithError({
|
|
||||||
totalDurationMs: executionResult?.metadata?.duration,
|
|
||||||
error: { message: errorMessage },
|
|
||||||
traceSpans: executionResult?.logs as any,
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
@@ -502,7 +491,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
{ status: isTimeout ? 408 : 500 }
|
{ status: 500 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -548,16 +537,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
const encoder = new TextEncoder()
|
const encoder = new TextEncoder()
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
let isStreamClosed = false
|
let isStreamClosed = false
|
||||||
let isTimedOut = false
|
|
||||||
|
|
||||||
const syncTimeout = preprocessResult.executionTimeout?.sync
|
|
||||||
let timeoutId: NodeJS.Timeout | undefined
|
|
||||||
if (syncTimeout) {
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
isTimedOut = true
|
|
||||||
abortController.abort()
|
|
||||||
}, syncTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = new ReadableStream<Uint8Array>({
|
const stream = new ReadableStream<Uint8Array>({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
@@ -784,35 +763,16 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.status === 'cancelled') {
|
if (result.status === 'cancelled') {
|
||||||
if (isTimedOut && syncTimeout) {
|
logger.info(`[${requestId}] Workflow execution was cancelled`)
|
||||||
const timeoutErrorMessage = getTimeoutErrorMessage(null, syncTimeout)
|
sendEvent({
|
||||||
logger.info(`[${requestId}] Workflow execution timed out`, { timeoutMs: syncTimeout })
|
type: 'execution:cancelled',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
await loggingSession.markAsFailed(timeoutErrorMessage)
|
executionId,
|
||||||
|
workflowId,
|
||||||
sendEvent({
|
data: {
|
||||||
type: 'execution:error',
|
duration: result.metadata?.duration || 0,
|
||||||
timestamp: new Date().toISOString(),
|
},
|
||||||
executionId,
|
})
|
||||||
workflowId,
|
|
||||||
data: {
|
|
||||||
error: timeoutErrorMessage,
|
|
||||||
duration: result.metadata?.duration || 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
logger.info(`[${requestId}] Workflow execution was cancelled`)
|
|
||||||
|
|
||||||
sendEvent({
|
|
||||||
type: 'execution:cancelled',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
executionId,
|
|
||||||
workflowId,
|
|
||||||
data: {
|
|
||||||
duration: result.metadata?.duration || 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -839,23 +799,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
// Cleanup base64 cache for this execution
|
// Cleanup base64 cache for this execution
|
||||||
await cleanupExecutionBase64Cache(executionId)
|
await cleanupExecutionBase64Cache(executionId)
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const isTimeout = isTimeoutError(error) || isTimedOut
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||||
const errorMessage = isTimeout
|
logger.error(`[${requestId}] SSE execution failed: ${errorMessage}`)
|
||||||
? getTimeoutErrorMessage(error, syncTimeout)
|
|
||||||
: error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: 'Unknown error'
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] SSE execution failed: ${errorMessage}`, { isTimeout })
|
|
||||||
|
|
||||||
const executionResult = hasExecutionResult(error) ? error.executionResult : undefined
|
const executionResult = hasExecutionResult(error) ? error.executionResult : undefined
|
||||||
|
|
||||||
await loggingSession.safeCompleteWithError({
|
|
||||||
totalDurationMs: executionResult?.metadata?.duration,
|
|
||||||
error: { message: errorMessage },
|
|
||||||
traceSpans: executionResult?.logs as any,
|
|
||||||
})
|
|
||||||
|
|
||||||
sendEvent({
|
sendEvent({
|
||||||
type: 'execution:error',
|
type: 'execution:error',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -867,18 +815,18 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
if (timeoutId) clearTimeout(timeoutId)
|
|
||||||
if (!isStreamClosed) {
|
if (!isStreamClosed) {
|
||||||
try {
|
try {
|
||||||
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
||||||
controller.close()
|
controller.close()
|
||||||
} catch {}
|
} catch {
|
||||||
|
// Stream already closed - nothing to do
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
isStreamClosed = true
|
isStreamClosed = true
|
||||||
if (timeoutId) clearTimeout(timeoutId)
|
|
||||||
logger.info(`[${requestId}] Client aborted SSE stream, signalling cancellation`)
|
logger.info(`[${requestId}] Client aborted SSE stream, signalling cancellation`)
|
||||||
abortController.abort()
|
abortController.abort()
|
||||||
markExecutionCancelled(executionId).catch(() => {})
|
markExecutionCancelled(executionId).catch(() => {})
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ describe('Workspace Invitations API Route', () => {
|
|||||||
inArray: vi.fn().mockImplementation((field, values) => ({ type: 'inArray', field, values })),
|
inArray: vi.fn().mockImplementation((field, values) => ({ type: 'inArray', field, values })),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@/executor/utils/permission-check', () => ({
|
vi.doMock('@/ee/access-control/utils/permission-check', () => ({
|
||||||
validateInvitationsAllowed: vi.fn().mockResolvedValue(undefined),
|
validateInvitationsAllowed: vi.fn().mockResolvedValue(undefined),
|
||||||
InvitationsNotAllowedError: class InvitationsNotAllowedError extends Error {
|
InvitationsNotAllowedError: class InvitationsNotAllowedError extends Error {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { getFromEmailAddress } from '@/lib/messaging/email/utils'
|
|||||||
import {
|
import {
|
||||||
InvitationsNotAllowedError,
|
InvitationsNotAllowedError,
|
||||||
validateInvitationsAllowed,
|
validateInvitationsAllowed,
|
||||||
} from '@/executor/utils/permission-check'
|
} from '@/ee/access-control/utils/permission-check'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
@@ -38,7 +38,6 @@ export async function GET(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all workspaces where the user has permissions
|
|
||||||
const userWorkspaces = await db
|
const userWorkspaces = await db
|
||||||
.select({ id: workspace.id })
|
.select({ id: workspace.id })
|
||||||
.from(workspace)
|
.from(workspace)
|
||||||
@@ -55,10 +54,8 @@ export async function GET(req: NextRequest) {
|
|||||||
return NextResponse.json({ invitations: [] })
|
return NextResponse.json({ invitations: [] })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all workspaceIds where the user is a member
|
|
||||||
const workspaceIds = userWorkspaces.map((w) => w.id)
|
const workspaceIds = userWorkspaces.map((w) => w.id)
|
||||||
|
|
||||||
// Find all invitations for those workspaces
|
|
||||||
const invitations = await db
|
const invitations = await db
|
||||||
.select()
|
.select()
|
||||||
.from(workspaceInvitation)
|
.from(workspaceInvitation)
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import {
|
|||||||
ChatMessageContainer,
|
ChatMessageContainer,
|
||||||
EmailAuth,
|
EmailAuth,
|
||||||
PasswordAuth,
|
PasswordAuth,
|
||||||
SSOAuth,
|
|
||||||
VoiceInterface,
|
VoiceInterface,
|
||||||
} from '@/app/chat/components'
|
} from '@/app/chat/components'
|
||||||
import { CHAT_ERROR_MESSAGES, CHAT_REQUEST_TIMEOUT_MS } from '@/app/chat/constants'
|
import { CHAT_ERROR_MESSAGES, CHAT_REQUEST_TIMEOUT_MS } from '@/app/chat/constants'
|
||||||
import { useAudioStreaming, useChatStreaming } from '@/app/chat/hooks'
|
import { useAudioStreaming, useChatStreaming } from '@/app/chat/hooks'
|
||||||
|
import SSOAuth from '@/ee/sso/components/sso-auth'
|
||||||
|
|
||||||
const logger = createLogger('ChatClient')
|
const logger = createLogger('ChatClient')
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export { default as EmailAuth } from './auth/email/email-auth'
|
export { default as EmailAuth } from './auth/email/email-auth'
|
||||||
export { default as PasswordAuth } from './auth/password/password-auth'
|
export { default as PasswordAuth } from './auth/password/password-auth'
|
||||||
export { default as SSOAuth } from './auth/sso/sso-auth'
|
|
||||||
export { ChatErrorState } from './error-state/error-state'
|
export { ChatErrorState } from './error-state/error-state'
|
||||||
export { ChatHeader } from './header/header'
|
export { ChatHeader } from './header/header'
|
||||||
export { ChatInput } from './input/input'
|
export { ChatInput } from './input/input'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
|
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
|
||||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
import { Knowledge } from './knowledge'
|
import { Knowledge } from './knowledge'
|
||||||
|
|
||||||
interface KnowledgePageProps {
|
interface KnowledgePageProps {
|
||||||
@@ -23,7 +23,6 @@ export default async function KnowledgePage({ params }: KnowledgePageProps) {
|
|||||||
redirect('/')
|
redirect('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check permission group restrictions
|
|
||||||
const permissionConfig = await getUserPermissionConfig(session.user.id)
|
const permissionConfig = await getUserPermissionConfig(session.user.id)
|
||||||
if (permissionConfig?.hideKnowledgeBaseTab) {
|
if (permissionConfig?.hideKnowledgeBaseTab) {
|
||||||
redirect(`/workspace/${workspaceId}`)
|
redirect(`/workspace/${workspaceId}`)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants'
|
import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
|
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||||
import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-spans'
|
import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||||
import {
|
import {
|
||||||
ExecutionSnapshot,
|
ExecutionSnapshot,
|
||||||
@@ -453,7 +454,7 @@ export const LogDetails = memo(function LogDetails({
|
|||||||
Duration
|
Duration
|
||||||
</span>
|
</span>
|
||||||
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>
|
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>
|
||||||
{log.duration || '—'}
|
{formatDuration(log.duration, { precision: 2 }) || '—'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import Link from 'next/link'
|
|||||||
import { List, type RowComponentProps, useListRef } from 'react-window'
|
import { List, type RowComponentProps, useListRef } from 'react-window'
|
||||||
import { Badge, buttonVariants } from '@/components/emcn'
|
import { Badge, buttonVariants } from '@/components/emcn'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
|
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||||
import {
|
import {
|
||||||
DELETED_WORKFLOW_COLOR,
|
DELETED_WORKFLOW_COLOR,
|
||||||
DELETED_WORKFLOW_LABEL,
|
DELETED_WORKFLOW_LABEL,
|
||||||
formatDate,
|
formatDate,
|
||||||
formatDuration,
|
|
||||||
getDisplayStatus,
|
getDisplayStatus,
|
||||||
LOG_COLUMNS,
|
LOG_COLUMNS,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
@@ -113,7 +113,7 @@ const LogRow = memo(
|
|||||||
|
|
||||||
<div className={`${LOG_COLUMNS.duration.width} ${LOG_COLUMNS.duration.minWidth}`}>
|
<div className={`${LOG_COLUMNS.duration.width} ${LOG_COLUMNS.duration.minWidth}`}>
|
||||||
<Badge variant='default' className='rounded-[6px] px-[9px] py-[2px] text-[12px]'>
|
<Badge variant='default' className='rounded-[6px] px-[9px] py-[2px] text-[12px]'>
|
||||||
{formatDuration(log.duration) || '—'}
|
{formatDuration(log.duration, { precision: 2 }) || '—'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { Badge } from '@/components/emcn'
|
import { Badge } from '@/components/emcn'
|
||||||
|
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||||
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
|
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
|
||||||
import { getBlock } from '@/blocks/registry'
|
import { getBlock } from '@/blocks/registry'
|
||||||
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||||
@@ -362,47 +363,14 @@ export function mapToExecutionLogAlt(log: RawLogResponse): ExecutionLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Format duration for display in logs UI
|
|
||||||
* If duration is under 1 second, displays as milliseconds (e.g., "500ms")
|
|
||||||
* If duration is 1 second or more, displays as seconds (e.g., "1.23s")
|
|
||||||
* @param duration - Duration string (e.g., "500ms") or null
|
|
||||||
* @returns Formatted duration string or null
|
|
||||||
*/
|
|
||||||
export function formatDuration(duration: string | null): string | null {
|
|
||||||
if (!duration) return null
|
|
||||||
|
|
||||||
// Extract numeric value from duration string (e.g., "500ms" -> 500)
|
|
||||||
const ms = Number.parseInt(duration.replace(/[^0-9]/g, ''), 10)
|
|
||||||
|
|
||||||
if (!Number.isFinite(ms)) return duration
|
|
||||||
|
|
||||||
if (ms < 1000) {
|
|
||||||
return `${ms}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to seconds with up to 2 decimal places
|
|
||||||
const seconds = ms / 1000
|
|
||||||
return `${seconds.toFixed(2).replace(/\.?0+$/, '')}s`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format latency value for display in dashboard UI
|
* Format latency value for display in dashboard UI
|
||||||
* If latency is under 1 second, displays as milliseconds (e.g., "500ms")
|
|
||||||
* If latency is 1 second or more, displays as seconds (e.g., "1.23s")
|
|
||||||
* @param ms - Latency in milliseconds (number)
|
* @param ms - Latency in milliseconds (number)
|
||||||
* @returns Formatted latency string
|
* @returns Formatted latency string
|
||||||
*/
|
*/
|
||||||
export function formatLatency(ms: number): string {
|
export function formatLatency(ms: number): string {
|
||||||
if (!Number.isFinite(ms) || ms <= 0) return '—'
|
if (!Number.isFinite(ms) || ms <= 0) return '—'
|
||||||
|
return formatDuration(ms, { precision: 2 }) ?? '—'
|
||||||
if (ms < 1000) {
|
|
||||||
return `${Math.round(ms)}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to seconds with up to 2 decimal places
|
|
||||||
const seconds = ms / 1000
|
|
||||||
return `${seconds.toFixed(2).replace(/\.?0+$/, '')}s`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatDate = (dateString: string) => {
|
export const formatDate = (dateString: string) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getSession } from '@/lib/auth'
|
|||||||
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
|
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
|
||||||
import type { Template as WorkspaceTemplate } from '@/app/workspace/[workspaceId]/templates/templates'
|
import type { Template as WorkspaceTemplate } from '@/app/workspace/[workspaceId]/templates/templates'
|
||||||
import Templates from '@/app/workspace/[workspaceId]/templates/templates'
|
import Templates from '@/app/workspace/[workspaceId]/templates/templates'
|
||||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
|
|
||||||
interface TemplatesPageProps {
|
interface TemplatesPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { ChevronUp } from 'lucide-react'
|
import { ChevronUp } from 'lucide-react'
|
||||||
|
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||||
import { CopilotMarkdownRenderer } from '../markdown-renderer'
|
import { CopilotMarkdownRenderer } from '../markdown-renderer'
|
||||||
|
|
||||||
/** Removes thinking tags (raw or escaped) and special tags from streamed content */
|
/** Removes thinking tags (raw or escaped) and special tags from streamed content */
|
||||||
@@ -241,15 +242,11 @@ export function ThinkingBlock({
|
|||||||
return () => window.clearInterval(intervalId)
|
return () => window.clearInterval(intervalId)
|
||||||
}, [isStreaming, isExpanded, userHasScrolledAway])
|
}, [isStreaming, isExpanded, userHasScrolledAway])
|
||||||
|
|
||||||
/** Formats duration in milliseconds to seconds (minimum 1s) */
|
|
||||||
const formatDuration = (ms: number) => {
|
|
||||||
const seconds = Math.max(1, Math.round(ms / 1000))
|
|
||||||
return `${seconds}s`
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasContent = cleanContent.length > 0
|
const hasContent = cleanContent.length > 0
|
||||||
const isThinkingDone = !isStreaming || hasFollowingContent || hasSpecialTags
|
const isThinkingDone = !isStreaming || hasFollowingContent || hasSpecialTags
|
||||||
const durationText = `${label} for ${formatDuration(duration)}`
|
// Round to nearest second (minimum 1s) to match original behavior
|
||||||
|
const roundedMs = Math.max(1000, Math.round(duration / 1000) * 1000)
|
||||||
|
const durationText = `${label} for ${formatDuration(roundedMs)}`
|
||||||
|
|
||||||
const getStreamingLabel = (lbl: string) => {
|
const getStreamingLabel = (lbl: string) => {
|
||||||
if (lbl === 'Thought') return 'Thinking'
|
if (lbl === 'Thought') return 'Thinking'
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
hasInterrupt as hasInterruptFromConfig,
|
hasInterrupt as hasInterruptFromConfig,
|
||||||
isSpecialTool as isSpecialToolFromConfig,
|
isSpecialTool as isSpecialToolFromConfig,
|
||||||
} from '@/lib/copilot/tools/client/ui-config'
|
} from '@/lib/copilot/tools/client/ui-config'
|
||||||
|
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||||
import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
|
import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
|
||||||
import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming'
|
import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming'
|
||||||
import { ThinkingBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block'
|
import { ThinkingBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block'
|
||||||
@@ -848,13 +849,10 @@ const SubagentContentRenderer = memo(function SubagentContentRenderer({
|
|||||||
(allParsed.options && Object.keys(allParsed.options).length > 0)
|
(allParsed.options && Object.keys(allParsed.options).length > 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
const formatDuration = (ms: number) => {
|
|
||||||
const seconds = Math.max(1, Math.round(ms / 1000))
|
|
||||||
return `${seconds}s`
|
|
||||||
}
|
|
||||||
|
|
||||||
const outerLabel = getSubagentCompletionLabel(toolCall.name)
|
const outerLabel = getSubagentCompletionLabel(toolCall.name)
|
||||||
const durationText = `${outerLabel} for ${formatDuration(duration)}`
|
// Round to nearest second (minimum 1s) to match original behavior
|
||||||
|
const roundedMs = Math.max(1000, Math.round(duration / 1000) * 1000)
|
||||||
|
const durationText = `${outerLabel} for ${formatDuration(roundedMs)}`
|
||||||
|
|
||||||
const renderCollapsibleContent = () => (
|
const renderCollapsibleContent = () => (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@/components/emcn'
|
} from '@/components/emcn'
|
||||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||||
|
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||||
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
|
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
|
||||||
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
|
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
|
||||||
import {
|
import {
|
||||||
@@ -43,7 +44,6 @@ import {
|
|||||||
type EntryNode,
|
type EntryNode,
|
||||||
type ExecutionGroup,
|
type ExecutionGroup,
|
||||||
flattenBlockEntriesOnly,
|
flattenBlockEntriesOnly,
|
||||||
formatDuration,
|
|
||||||
getBlockColor,
|
getBlockColor,
|
||||||
getBlockIcon,
|
getBlockIcon,
|
||||||
groupEntriesByExecution,
|
groupEntriesByExecution,
|
||||||
@@ -128,7 +128,7 @@ const BlockRow = memo(function BlockRow({
|
|||||||
<StatusDisplay
|
<StatusDisplay
|
||||||
isRunning={isRunning}
|
isRunning={isRunning}
|
||||||
isCanceled={isCanceled}
|
isCanceled={isCanceled}
|
||||||
formattedDuration={formatDuration(entry.durationMs)}
|
formattedDuration={formatDuration(entry.durationMs, { precision: 2 }) ?? '-'}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,7 +201,7 @@ const IterationNodeRow = memo(function IterationNodeRow({
|
|||||||
<StatusDisplay
|
<StatusDisplay
|
||||||
isRunning={hasRunningChild}
|
isRunning={hasRunningChild}
|
||||||
isCanceled={hasCanceledChild}
|
isCanceled={hasCanceledChild}
|
||||||
formattedDuration={formatDuration(entry.durationMs)}
|
formattedDuration={formatDuration(entry.durationMs, { precision: 2 }) ?? '-'}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,7 +314,7 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
|
|||||||
<StatusDisplay
|
<StatusDisplay
|
||||||
isRunning={hasRunningDescendant}
|
isRunning={hasRunningDescendant}
|
||||||
isCanceled={hasCanceledDescendant}
|
isCanceled={hasCanceledDescendant}
|
||||||
formattedDuration={formatDuration(entry.durationMs)}
|
formattedDuration={formatDuration(entry.durationMs, { precision: 2 }) ?? '-'}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,17 +53,6 @@ export function getBlockColor(blockType: string): string {
|
|||||||
return '#6b7280'
|
return '#6b7280'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats duration from milliseconds to readable format
|
|
||||||
*/
|
|
||||||
export function formatDuration(ms?: number): string {
|
|
||||||
if (ms === undefined || ms === null) return '-'
|
|
||||||
if (ms < 1000) {
|
|
||||||
return `${Math.round(ms)}ms`
|
|
||||||
}
|
|
||||||
return `${(ms / 1000).toFixed(2)}s`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if a keyboard event originated from a text-editable element
|
* Determines if a keyboard event originated from a text-editable element
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
} from '@/components/emcn'
|
} from '@/components/emcn'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
|
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||||
import { formatEditSequence } from '@/lib/workflows/training/compute-edit-sequence'
|
import { formatEditSequence } from '@/lib/workflows/training/compute-edit-sequence'
|
||||||
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
|
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
|
||||||
@@ -575,7 +576,9 @@ export function TrainingModal() {
|
|||||||
<span className='text-[var(--text-muted)]'>Duration:</span>{' '}
|
<span className='text-[var(--text-muted)]'>Duration:</span>{' '}
|
||||||
<span className='text-[var(--text-secondary)]'>
|
<span className='text-[var(--text-secondary)]'>
|
||||||
{dataset.metadata?.duration
|
{dataset.metadata?.duration
|
||||||
? `${(dataset.metadata.duration / 1000).toFixed(1)}s`
|
? formatDuration(dataset.metadata.duration, {
|
||||||
|
precision: 1,
|
||||||
|
})
|
||||||
: 'N/A'}
|
: 'N/A'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { useExecutionStore } from '@/stores/execution'
|
|||||||
import { useNotificationStore } from '@/stores/notifications'
|
import { useNotificationStore } from '@/stores/notifications'
|
||||||
import { useVariablesStore } from '@/stores/panel'
|
import { useVariablesStore } from '@/stores/panel'
|
||||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||||
import { useTerminalConsoleStore } from '@/stores/terminal'
|
import { type ConsoleEntry, useTerminalConsoleStore } from '@/stores/terminal'
|
||||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||||
@@ -1153,29 +1153,30 @@ export function useWorkflowExecution() {
|
|||||||
logs: accumulatedBlockLogs,
|
logs: accumulatedBlockLogs,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeWorkflowId) {
|
// Only add workflow-level error if no blocks have executed yet
|
||||||
cancelRunningEntries(activeWorkflowId)
|
// This catches pre-execution errors (validation, serialization, etc.)
|
||||||
}
|
// Block execution errors are already logged via onBlockError callback
|
||||||
|
const { entries } = useTerminalConsoleStore.getState()
|
||||||
|
const existingLogs = entries.filter(
|
||||||
|
(log: ConsoleEntry) => log.executionId === executionId
|
||||||
|
)
|
||||||
|
|
||||||
addConsole({
|
if (existingLogs.length === 0) {
|
||||||
input: {},
|
// No blocks executed yet - this is a pre-execution error
|
||||||
output: {},
|
addConsole({
|
||||||
success: false,
|
input: {},
|
||||||
error: data.error,
|
output: {},
|
||||||
durationMs: data.duration || 0,
|
success: false,
|
||||||
startedAt: new Date(Date.now() - (data.duration || 0)).toISOString(),
|
error: data.error,
|
||||||
endedAt: new Date().toISOString(),
|
durationMs: data.duration || 0,
|
||||||
workflowId: activeWorkflowId,
|
startedAt: new Date(Date.now() - (data.duration || 0)).toISOString(),
|
||||||
blockId: 'workflow-error',
|
endedAt: new Date().toISOString(),
|
||||||
executionId,
|
workflowId: activeWorkflowId,
|
||||||
blockName: 'Workflow Error',
|
blockId: 'validation',
|
||||||
blockType: 'error',
|
executionId,
|
||||||
})
|
blockName: 'Workflow Validation',
|
||||||
},
|
blockType: 'validation',
|
||||||
|
})
|
||||||
onExecutionCancelled: () => {
|
|
||||||
if (activeWorkflowId) {
|
|
||||||
cancelRunningEntries(activeWorkflowId)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1717,28 +1718,13 @@ export function useWorkflowExecution() {
|
|||||||
'Workflow was modified. Run the workflow again to enable running from block.',
|
'Workflow was modified. Run the workflow again to enable running from block.',
|
||||||
workflowId,
|
workflowId,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
addNotification({
|
||||||
|
level: 'error',
|
||||||
|
message: data.error || 'Run from block failed',
|
||||||
|
workflowId,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelRunningEntries(workflowId)
|
|
||||||
|
|
||||||
addConsole({
|
|
||||||
input: {},
|
|
||||||
output: {},
|
|
||||||
success: false,
|
|
||||||
error: data.error,
|
|
||||||
durationMs: data.duration || 0,
|
|
||||||
startedAt: new Date(Date.now() - (data.duration || 0)).toISOString(),
|
|
||||||
endedAt: new Date().toISOString(),
|
|
||||||
workflowId,
|
|
||||||
blockId: 'workflow-error',
|
|
||||||
executionId,
|
|
||||||
blockName: 'Workflow Error',
|
|
||||||
blockType: 'error',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
onExecutionCancelled: () => {
|
|
||||||
cancelRunningEntries(workflowId)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -246,7 +246,6 @@ export function CredentialSets() {
|
|||||||
setNewSetDescription('')
|
setNewSetDescription('')
|
||||||
setNewSetProvider('google-email')
|
setNewSetProvider('google-email')
|
||||||
|
|
||||||
// Open detail view for the newly created group
|
|
||||||
if (result?.credentialSet) {
|
if (result?.credentialSet) {
|
||||||
setViewingSet(result.credentialSet)
|
setViewingSet(result.credentialSet)
|
||||||
}
|
}
|
||||||
@@ -336,7 +335,6 @@ export function CredentialSets() {
|
|||||||
email,
|
email,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start 60s cooldown
|
|
||||||
setResendCooldowns((prev) => ({ ...prev, [invitationId]: 60 }))
|
setResendCooldowns((prev) => ({ ...prev, [invitationId]: 60 }))
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setResendCooldowns((prev) => {
|
setResendCooldowns((prev) => {
|
||||||
@@ -393,7 +391,6 @@ export function CredentialSets() {
|
|||||||
return <GmailIcon className='h-4 w-4' />
|
return <GmailIcon className='h-4 w-4' />
|
||||||
}
|
}
|
||||||
|
|
||||||
// All hooks must be called before any early returns
|
|
||||||
const activeMemberships = useMemo(
|
const activeMemberships = useMemo(
|
||||||
() => memberships.filter((m) => m.status === 'active'),
|
() => memberships.filter((m) => m.status === 'active'),
|
||||||
[memberships]
|
[memberships]
|
||||||
@@ -447,7 +444,6 @@ export function CredentialSets() {
|
|||||||
<div className='flex h-full flex-col gap-[16px]'>
|
<div className='flex h-full flex-col gap-[16px]'>
|
||||||
<div className='min-h-0 flex-1 overflow-y-auto'>
|
<div className='min-h-0 flex-1 overflow-y-auto'>
|
||||||
<div className='flex flex-col gap-[16px]'>
|
<div className='flex flex-col gap-[16px]'>
|
||||||
{/* Group Info */}
|
|
||||||
<div className='flex items-center gap-[16px]'>
|
<div className='flex items-center gap-[16px]'>
|
||||||
<div className='flex items-center gap-[8px]'>
|
<div className='flex items-center gap-[8px]'>
|
||||||
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
|
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||||
@@ -471,7 +467,6 @@ export function CredentialSets() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Invite Section - Email Tags Input */}
|
|
||||||
<div className='flex flex-col gap-[4px]'>
|
<div className='flex flex-col gap-[4px]'>
|
||||||
<div className='flex items-center gap-[8px]'>
|
<div className='flex items-center gap-[8px]'>
|
||||||
<TagInput
|
<TagInput
|
||||||
@@ -495,7 +490,6 @@ export function CredentialSets() {
|
|||||||
{emailError && <p className='text-[12px] text-[var(--text-error)]'>{emailError}</p>}
|
{emailError && <p className='text-[12px] text-[var(--text-error)]'>{emailError}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Members List - styled like team members */}
|
|
||||||
<div className='flex flex-col gap-[16px]'>
|
<div className='flex flex-col gap-[16px]'>
|
||||||
<h4 className='font-medium text-[14px] text-[var(--text-primary)]'>Members</h4>
|
<h4 className='font-medium text-[14px] text-[var(--text-primary)]'>Members</h4>
|
||||||
|
|
||||||
@@ -519,7 +513,6 @@ export function CredentialSets() {
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className='flex flex-col gap-[16px]'>
|
<div className='flex flex-col gap-[16px]'>
|
||||||
{/* Active Members */}
|
|
||||||
{activeMembers.map((member) => {
|
{activeMembers.map((member) => {
|
||||||
const name = member.userName || 'Unknown'
|
const name = member.userName || 'Unknown'
|
||||||
const avatarInitial = name.charAt(0).toUpperCase()
|
const avatarInitial = name.charAt(0).toUpperCase()
|
||||||
@@ -572,7 +565,6 @@ export function CredentialSets() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Pending Invitations */}
|
|
||||||
{pendingInvitations.map((invitation) => {
|
{pendingInvitations.map((invitation) => {
|
||||||
const email = invitation.email || 'Unknown'
|
const email = invitation.email || 'Unknown'
|
||||||
const emailPrefix = email.split('@')[0]
|
const emailPrefix = email.split('@')[0]
|
||||||
@@ -641,7 +633,6 @@ export function CredentialSets() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Actions */}
|
|
||||||
<div className='mt-auto flex items-center justify-end'>
|
<div className='mt-auto flex items-center justify-end'>
|
||||||
<Button onClick={handleBackToList} variant='tertiary'>
|
<Button onClick={handleBackToList} variant='tertiary'>
|
||||||
Back
|
Back
|
||||||
@@ -822,7 +813,6 @@ export function CredentialSets() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create Polling Group Modal */}
|
|
||||||
<Modal open={showCreateModal} onOpenChange={handleCloseCreateModal}>
|
<Modal open={showCreateModal} onOpenChange={handleCloseCreateModal}>
|
||||||
<ModalContent size='sm'>
|
<ModalContent size='sm'>
|
||||||
<ModalHeader>Create Polling Group</ModalHeader>
|
<ModalHeader>Create Polling Group</ModalHeader>
|
||||||
@@ -895,7 +885,6 @@ export function CredentialSets() {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Leave Confirmation Modal */}
|
|
||||||
<Modal open={!!leavingMembership} onOpenChange={() => setLeavingMembership(null)}>
|
<Modal open={!!leavingMembership} onOpenChange={() => setLeavingMembership(null)}>
|
||||||
<ModalContent size='sm'>
|
<ModalContent size='sm'>
|
||||||
<ModalHeader>Leave Polling Group</ModalHeader>
|
<ModalHeader>Leave Polling Group</ModalHeader>
|
||||||
@@ -923,7 +912,6 @@ export function CredentialSets() {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
|
||||||
<Modal open={!!deletingSet} onOpenChange={() => setDeletingSet(null)}>
|
<Modal open={!!deletingSet} onOpenChange={() => setDeletingSet(null)}>
|
||||||
<ModalContent size='sm'>
|
<ModalContent size='sm'>
|
||||||
<ModalHeader>Delete Polling Group</ModalHeader>
|
<ModalHeader>Delete Polling Group</ModalHeader>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export { AccessControl } from './access-control/access-control'
|
|
||||||
export { ApiKeys } from './api-keys/api-keys'
|
export { ApiKeys } from './api-keys/api-keys'
|
||||||
export { BYOK } from './byok/byok'
|
export { BYOK } from './byok/byok'
|
||||||
export { Copilot } from './copilot/copilot'
|
export { Copilot } from './copilot/copilot'
|
||||||
@@ -10,7 +9,6 @@ export { Files as FileUploads } from './files/files'
|
|||||||
export { General } from './general/general'
|
export { General } from './general/general'
|
||||||
export { Integrations } from './integrations/integrations'
|
export { Integrations } from './integrations/integrations'
|
||||||
export { MCP } from './mcp/mcp'
|
export { MCP } from './mcp/mcp'
|
||||||
export { SSO } from './sso/sso'
|
|
||||||
export { Subscription } from './subscription/subscription'
|
export { Subscription } from './subscription/subscription'
|
||||||
export { TeamManagement } from './team-management/team-management'
|
export { TeamManagement } from './team-management/team-management'
|
||||||
export { WorkflowMcpServers } from './workflow-mcp-servers/workflow-mcp-servers'
|
export { WorkflowMcpServers } from './workflow-mcp-servers/workflow-mcp-servers'
|
||||||
|
|||||||
@@ -407,14 +407,12 @@ export function MCP({ initialServerId }: MCPProps) {
|
|||||||
const [urlScrollLeft, setUrlScrollLeft] = useState(0)
|
const [urlScrollLeft, setUrlScrollLeft] = useState(0)
|
||||||
const [headerScrollLeft, setHeaderScrollLeft] = useState<Record<string, number>>({})
|
const [headerScrollLeft, setHeaderScrollLeft] = useState<Record<string, number>>({})
|
||||||
|
|
||||||
// Auto-select server when initialServerId is provided
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialServerId && servers.some((s) => s.id === initialServerId)) {
|
if (initialServerId && servers.some((s) => s.id === initialServerId)) {
|
||||||
setSelectedServerId(initialServerId)
|
setSelectedServerId(initialServerId)
|
||||||
}
|
}
|
||||||
}, [initialServerId, servers])
|
}, [initialServerId, servers])
|
||||||
|
|
||||||
// Force refresh tools when entering server detail view to detect stale schemas
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedServerId) {
|
if (selectedServerId) {
|
||||||
forceRefreshTools(workspaceId)
|
forceRefreshTools(workspaceId)
|
||||||
@@ -717,7 +715,6 @@ export function MCP({ initialServerId }: MCPProps) {
|
|||||||
`Refreshed MCP server: ${serverId}, workflows updated: ${result.workflowsUpdated}`
|
`Refreshed MCP server: ${serverId}, workflows updated: ${result.workflowsUpdated}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// If the active workflow was updated, reload its subblock values from DB
|
|
||||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||||
if (activeWorkflowId && result.updatedWorkflowIds?.includes(activeWorkflowId)) {
|
if (activeWorkflowId && result.updatedWorkflowIds?.includes(activeWorkflowId)) {
|
||||||
logger.info(`Active workflow ${activeWorkflowId} was updated, reloading subblock values`)
|
logger.info(`Active workflow ${activeWorkflowId} was updated, reloading subblock values`)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
|
Building2,
|
||||||
Clock,
|
Clock,
|
||||||
Database,
|
Database,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
HeadphonesIcon,
|
HeadphonesIcon,
|
||||||
Server,
|
Server,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Timer,
|
|
||||||
Users,
|
Users,
|
||||||
Zap,
|
Zap,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -15,8 +15,8 @@ import type { PlanFeature } from '@/app/workspace/[workspaceId]/w/components/sid
|
|||||||
export const PRO_PLAN_FEATURES: PlanFeature[] = [
|
export const PRO_PLAN_FEATURES: PlanFeature[] = [
|
||||||
{ icon: Zap, text: '150 runs per minute (sync)' },
|
{ icon: Zap, text: '150 runs per minute (sync)' },
|
||||||
{ icon: Clock, text: '1,000 runs per minute (async)' },
|
{ icon: Clock, text: '1,000 runs per minute (async)' },
|
||||||
{ icon: Timer, text: '60 min sync execution limit' },
|
|
||||||
{ icon: HardDrive, text: '50GB file storage' },
|
{ icon: HardDrive, text: '50GB file storage' },
|
||||||
|
{ icon: Building2, text: 'Unlimited workspaces' },
|
||||||
{ icon: Users, text: 'Unlimited invites' },
|
{ icon: Users, text: 'Unlimited invites' },
|
||||||
{ icon: Database, text: 'Unlimited log retention' },
|
{ icon: Database, text: 'Unlimited log retention' },
|
||||||
]
|
]
|
||||||
@@ -24,8 +24,8 @@ export const PRO_PLAN_FEATURES: PlanFeature[] = [
|
|||||||
export const TEAM_PLAN_FEATURES: PlanFeature[] = [
|
export const TEAM_PLAN_FEATURES: PlanFeature[] = [
|
||||||
{ icon: Zap, text: '300 runs per minute (sync)' },
|
{ icon: Zap, text: '300 runs per minute (sync)' },
|
||||||
{ icon: Clock, text: '2,500 runs per minute (async)' },
|
{ icon: Clock, text: '2,500 runs per minute (async)' },
|
||||||
{ icon: Timer, text: '60 min sync execution limit' },
|
|
||||||
{ icon: HardDrive, text: '500GB file storage (pooled)' },
|
{ icon: HardDrive, text: '500GB file storage (pooled)' },
|
||||||
|
{ icon: Building2, text: 'Unlimited workspaces' },
|
||||||
{ icon: Users, text: 'Unlimited invites' },
|
{ icon: Users, text: 'Unlimited invites' },
|
||||||
{ icon: Database, text: 'Unlimited log retention' },
|
{ icon: Database, text: 'Unlimited log retention' },
|
||||||
{ icon: SlackMonoIcon, text: 'Dedicated Slack channel' },
|
{ icon: SlackMonoIcon, text: 'Dedicated Slack channel' },
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import { getEnv, isTruthy } from '@/lib/core/config/env'
|
|||||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||||
import { getUserRole } from '@/lib/workspaces/organization'
|
import { getUserRole } from '@/lib/workspaces/organization'
|
||||||
import {
|
import {
|
||||||
AccessControl,
|
|
||||||
ApiKeys,
|
ApiKeys,
|
||||||
BYOK,
|
BYOK,
|
||||||
Copilot,
|
Copilot,
|
||||||
@@ -53,15 +52,16 @@ import {
|
|||||||
General,
|
General,
|
||||||
Integrations,
|
Integrations,
|
||||||
MCP,
|
MCP,
|
||||||
SSO,
|
|
||||||
Subscription,
|
Subscription,
|
||||||
TeamManagement,
|
TeamManagement,
|
||||||
WorkflowMcpServers,
|
WorkflowMcpServers,
|
||||||
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components'
|
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components'
|
||||||
import { TemplateProfile } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/template-profile/template-profile'
|
import { TemplateProfile } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/template-profile/template-profile'
|
||||||
|
import { AccessControl } from '@/ee/access-control/components/access-control'
|
||||||
|
import { SSO } from '@/ee/sso/components/sso-settings'
|
||||||
|
import { ssoKeys, useSSOProviders } from '@/ee/sso/hooks/sso'
|
||||||
import { generalSettingsKeys, useGeneralSettings } from '@/hooks/queries/general-settings'
|
import { generalSettingsKeys, useGeneralSettings } from '@/hooks/queries/general-settings'
|
||||||
import { organizationKeys, useOrganizations } from '@/hooks/queries/organization'
|
import { organizationKeys, useOrganizations } from '@/hooks/queries/organization'
|
||||||
import { ssoKeys, useSSOProviders } from '@/hooks/queries/sso'
|
|
||||||
import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription'
|
import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription'
|
||||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||||
import { useSettingsModalStore } from '@/stores/modals/settings/store'
|
import { useSettingsModalStore } from '@/stores/modals/settings/store'
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { checkUsageStatus } from '@/lib/billing/calculations/usage-monitor'
|
|||||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
||||||
import { RateLimiter } from '@/lib/core/rate-limiter'
|
import { RateLimiter } from '@/lib/core/rate-limiter'
|
||||||
import { decryptSecret } from '@/lib/core/security/encryption'
|
import { decryptSecret } from '@/lib/core/security/encryption'
|
||||||
|
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
|
import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
|
||||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||||
@@ -227,12 +228,6 @@ async function deliverWebhook(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
if (ms < 1000) return `${ms}ms`
|
|
||||||
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
|
|
||||||
return `${(ms / 60000).toFixed(1)}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCost(cost?: Record<string, unknown>): string {
|
function formatCost(cost?: Record<string, unknown>): string {
|
||||||
if (!cost?.total) return 'N/A'
|
if (!cost?.total) return 'N/A'
|
||||||
const total = cost.total as number
|
const total = cost.total as number
|
||||||
@@ -302,7 +297,7 @@ async function deliverEmail(
|
|||||||
workflowName: payload.data.workflowName || 'Unknown Workflow',
|
workflowName: payload.data.workflowName || 'Unknown Workflow',
|
||||||
status: payload.data.status,
|
status: payload.data.status,
|
||||||
trigger: payload.data.trigger,
|
trigger: payload.data.trigger,
|
||||||
duration: formatDuration(payload.data.totalDurationMs),
|
duration: formatDuration(payload.data.totalDurationMs, { precision: 1 }) ?? '-',
|
||||||
cost: formatCost(payload.data.cost),
|
cost: formatCost(payload.data.cost),
|
||||||
logUrl,
|
logUrl,
|
||||||
alertReason,
|
alertReason,
|
||||||
@@ -315,7 +310,7 @@ async function deliverEmail(
|
|||||||
to: subscription.emailRecipients,
|
to: subscription.emailRecipients,
|
||||||
subject,
|
subject,
|
||||||
html,
|
html,
|
||||||
text: `${subject}\n${alertReason ? `\nReason: ${alertReason}\n` : ''}\nWorkflow: ${payload.data.workflowName}\nStatus: ${statusText}\nTrigger: ${payload.data.trigger}\nDuration: ${formatDuration(payload.data.totalDurationMs)}\nCost: ${formatCost(payload.data.cost)}\n\nView Log: ${logUrl}${includedDataText}`,
|
text: `${subject}\n${alertReason ? `\nReason: ${alertReason}\n` : ''}\nWorkflow: ${payload.data.workflowName}\nStatus: ${statusText}\nTrigger: ${payload.data.trigger}\nDuration: ${formatDuration(payload.data.totalDurationMs, { precision: 1 }) ?? '-'}\nCost: ${formatCost(payload.data.cost)}\n\nView Log: ${logUrl}${includedDataText}`,
|
||||||
emailType: 'notifications',
|
emailType: 'notifications',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -373,7 +368,10 @@ async function deliverSlack(
|
|||||||
fields: [
|
fields: [
|
||||||
{ type: 'mrkdwn', text: `*Status:*\n${payload.data.status}` },
|
{ type: 'mrkdwn', text: `*Status:*\n${payload.data.status}` },
|
||||||
{ type: 'mrkdwn', text: `*Trigger:*\n${payload.data.trigger}` },
|
{ type: 'mrkdwn', text: `*Trigger:*\n${payload.data.trigger}` },
|
||||||
{ type: 'mrkdwn', text: `*Duration:*\n${formatDuration(payload.data.totalDurationMs)}` },
|
{
|
||||||
|
type: 'mrkdwn',
|
||||||
|
text: `*Duration:*\n${formatDuration(payload.data.totalDurationMs, { precision: 1 }) ?? '-'}`,
|
||||||
|
},
|
||||||
{ type: 'mrkdwn', text: `*Cost:*\n${formatCost(payload.data.cost)}` },
|
{ type: 'mrkdwn', text: `*Cost:*\n${formatCost(payload.data.cost)}` },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||||
import type { ToolCallGroup, ToolCallState } from '@/lib/copilot/types'
|
import type { ToolCallGroup, ToolCallState } from '@/lib/copilot/types'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
|
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||||
|
|
||||||
interface ToolCallProps {
|
interface ToolCallProps {
|
||||||
toolCall: ToolCallState
|
toolCall: ToolCallState
|
||||||
@@ -225,11 +226,6 @@ export function ToolCallCompletion({ toolCall, isCompact = false }: ToolCallProp
|
|||||||
const isError = toolCall.state === 'error'
|
const isError = toolCall.state === 'error'
|
||||||
const isAborted = toolCall.state === 'aborted'
|
const isAborted = toolCall.state === 'aborted'
|
||||||
|
|
||||||
const formatDuration = (duration?: number) => {
|
|
||||||
if (!duration) return ''
|
|
||||||
return duration < 1000 ? `${duration}ms` : `${(duration / 1000).toFixed(1)}s`
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -279,7 +275,7 @@ export function ToolCallCompletion({ toolCall, isCompact = false }: ToolCallProp
|
|||||||
)}
|
)}
|
||||||
style={{ fontSize: '0.625rem' }}
|
style={{ fontSize: '0.625rem' }}
|
||||||
>
|
>
|
||||||
{formatDuration(toolCall.duration)}
|
{toolCall.duration ? formatDuration(toolCall.duration, { precision: 1 }) : ''}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
43
apps/sim/ee/LICENSE
Normal file
43
apps/sim/ee/LICENSE
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
Sim Enterprise License
|
||||||
|
|
||||||
|
Copyright (c) 2025-present Sim Studio, Inc.
|
||||||
|
|
||||||
|
This software and associated documentation files (the "Software") are licensed
|
||||||
|
under the following terms:
|
||||||
|
|
||||||
|
1. LICENSE GRANT
|
||||||
|
|
||||||
|
Subject to the terms of this license, Sim Studio, Inc. grants you a limited,
|
||||||
|
non-exclusive, non-transferable license to use the Software for:
|
||||||
|
|
||||||
|
- Development, testing, and evaluation purposes
|
||||||
|
- Internal non-production use
|
||||||
|
|
||||||
|
Production use of the Software requires a valid Sim Enterprise subscription.
|
||||||
|
|
||||||
|
2. RESTRICTIONS
|
||||||
|
|
||||||
|
You may not:
|
||||||
|
|
||||||
|
- Use the Software in production without a valid Enterprise subscription
|
||||||
|
- Modify, adapt, or create derivative works of the Software
|
||||||
|
- Redistribute, sublicense, or transfer the Software
|
||||||
|
- Remove or alter any proprietary notices in the Software
|
||||||
|
|
||||||
|
3. ENTERPRISE SUBSCRIPTION
|
||||||
|
|
||||||
|
Production deployment of enterprise features requires an active Sim Enterprise
|
||||||
|
subscription. Contact sales@simstudio.ai for licensing information.
|
||||||
|
|
||||||
|
4. DISCLAIMER
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
|
||||||
|
5. LIMITATION OF LIABILITY
|
||||||
|
|
||||||
|
IN NO EVENT SHALL SIM STUDIO, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
|
||||||
|
|
||||||
|
For questions about enterprise licensing, contact: sales@simstudio.ai
|
||||||
21
apps/sim/ee/README.md
Normal file
21
apps/sim/ee/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Sim Enterprise Edition
|
||||||
|
|
||||||
|
This directory contains enterprise features that require a Sim Enterprise subscription
|
||||||
|
for production use.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **SSO (Single Sign-On)**: OIDC and SAML authentication integration
|
||||||
|
- **Access Control**: Permission groups for fine-grained user access management
|
||||||
|
- **Credential Sets**: Shared credential pools for email polling workflows
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
See [LICENSE](./LICENSE) for terms. Development and testing use is permitted.
|
||||||
|
Production deployment requires an active Enterprise subscription.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Enterprise features are imported directly throughout the codebase. The `ee/` directory
|
||||||
|
is required at build time. Feature visibility is controlled at runtime via environment
|
||||||
|
variables (e.g., `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED`).
|
||||||
@@ -29,7 +29,6 @@ import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
|
|||||||
import { getUserColor } from '@/lib/workspaces/colors'
|
import { getUserColor } from '@/lib/workspaces/colors'
|
||||||
import { getUserRole } from '@/lib/workspaces/organization'
|
import { getUserRole } from '@/lib/workspaces/organization'
|
||||||
import { getAllBlocks } from '@/blocks'
|
import { getAllBlocks } from '@/blocks'
|
||||||
import { useOrganization, useOrganizations } from '@/hooks/queries/organization'
|
|
||||||
import {
|
import {
|
||||||
type PermissionGroup,
|
type PermissionGroup,
|
||||||
useBulkAddPermissionGroupMembers,
|
useBulkAddPermissionGroupMembers,
|
||||||
@@ -39,7 +38,8 @@ import {
|
|||||||
usePermissionGroups,
|
usePermissionGroups,
|
||||||
useRemovePermissionGroupMember,
|
useRemovePermissionGroupMember,
|
||||||
useUpdatePermissionGroup,
|
useUpdatePermissionGroup,
|
||||||
} from '@/hooks/queries/permission-groups'
|
} from '@/ee/access-control/hooks/permission-groups'
|
||||||
|
import { useOrganization, useOrganizations } from '@/hooks/queries/organization'
|
||||||
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
||||||
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
||||||
import { getAllProviderIds } from '@/providers/utils'
|
import { getAllProviderIds } from '@/providers/utils'
|
||||||
@@ -255,7 +255,6 @@ export function AccessControl() {
|
|||||||
queryEnabled
|
queryEnabled
|
||||||
)
|
)
|
||||||
|
|
||||||
// Show loading while dependencies load, or while permission groups query is pending
|
|
||||||
const isLoading = orgsLoading || subLoading || (queryEnabled && groupsLoading)
|
const isLoading = orgsLoading || subLoading || (queryEnabled && groupsLoading)
|
||||||
const { data: organization } = useOrganization(activeOrganization?.id || '')
|
const { data: organization } = useOrganization(activeOrganization?.id || '')
|
||||||
|
|
||||||
@@ -410,10 +409,8 @@ export function AccessControl() {
|
|||||||
}, [viewingGroup, editingConfig])
|
}, [viewingGroup, editingConfig])
|
||||||
|
|
||||||
const allBlocks = useMemo(() => {
|
const allBlocks = useMemo(() => {
|
||||||
// Filter out hidden blocks and start_trigger (which should never be disabled)
|
|
||||||
const blocks = getAllBlocks().filter((b) => !b.hideFromToolbar && b.type !== 'start_trigger')
|
const blocks = getAllBlocks().filter((b) => !b.hideFromToolbar && b.type !== 'start_trigger')
|
||||||
return blocks.sort((a, b) => {
|
return blocks.sort((a, b) => {
|
||||||
// Group by category: triggers first, then blocks, then tools
|
|
||||||
const categoryOrder = { triggers: 0, blocks: 1, tools: 2 }
|
const categoryOrder = { triggers: 0, blocks: 1, tools: 2 }
|
||||||
const catA = categoryOrder[a.category] ?? 3
|
const catA = categoryOrder[a.category] ?? 3
|
||||||
const catB = categoryOrder[b.category] ?? 3
|
const catB = categoryOrder[b.category] ?? 3
|
||||||
@@ -555,10 +552,9 @@ export function AccessControl() {
|
|||||||
}, [viewingGroup, editingConfig, activeOrganization?.id, updatePermissionGroup])
|
}, [viewingGroup, editingConfig, activeOrganization?.id, updatePermissionGroup])
|
||||||
|
|
||||||
const handleOpenAddMembersModal = useCallback(() => {
|
const handleOpenAddMembersModal = useCallback(() => {
|
||||||
const existingMemberUserIds = new Set(members.map((m) => m.userId))
|
|
||||||
setSelectedMemberIds(new Set())
|
setSelectedMemberIds(new Set())
|
||||||
setShowAddMembersModal(true)
|
setShowAddMembersModal(true)
|
||||||
}, [members])
|
}, [])
|
||||||
|
|
||||||
const handleAddSelectedMembers = useCallback(async () => {
|
const handleAddSelectedMembers = useCallback(async () => {
|
||||||
if (!viewingGroup || selectedMemberIds.size === 0) return
|
if (!viewingGroup || selectedMemberIds.size === 0) return
|
||||||
@@ -891,7 +887,6 @@ export function AccessControl() {
|
|||||||
prev
|
prev
|
||||||
? {
|
? {
|
||||||
...prev,
|
...prev,
|
||||||
// When deselecting all, keep start_trigger allowed (it should never be disabled)
|
|
||||||
allowedIntegrations: allAllowed ? ['start_trigger'] : null,
|
allowedIntegrations: allAllowed ? ['start_trigger'] : null,
|
||||||
}
|
}
|
||||||
: prev
|
: prev
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
|
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
|
||||||
import { fetchJson } from '@/hooks/selectors/helpers'
|
import { fetchJson } from '@/hooks/selectors/helpers'
|
||||||
@@ -11,55 +11,13 @@ import { isBillingEnabled } from '@/lib/core/config/feature-flags'
|
|||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { getUserRole } from '@/lib/workspaces/organization/utils'
|
import { getUserRole } from '@/lib/workspaces/organization/utils'
|
||||||
|
import { SSO_TRUSTED_PROVIDERS } from '@/ee/sso/constants'
|
||||||
|
import { useConfigureSSO, useSSOProviders } from '@/ee/sso/hooks/sso'
|
||||||
import { useOrganizations } from '@/hooks/queries/organization'
|
import { useOrganizations } from '@/hooks/queries/organization'
|
||||||
import { useConfigureSSO, useSSOProviders } from '@/hooks/queries/sso'
|
|
||||||
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
||||||
|
|
||||||
const logger = createLogger('SSO')
|
const logger = createLogger('SSO')
|
||||||
|
|
||||||
const TRUSTED_SSO_PROVIDERS = [
|
|
||||||
'okta',
|
|
||||||
'okta-saml',
|
|
||||||
'okta-prod',
|
|
||||||
'okta-dev',
|
|
||||||
'okta-staging',
|
|
||||||
'okta-test',
|
|
||||||
'azure-ad',
|
|
||||||
'azure-active-directory',
|
|
||||||
'azure-corp',
|
|
||||||
'azure-enterprise',
|
|
||||||
'adfs',
|
|
||||||
'adfs-company',
|
|
||||||
'adfs-corp',
|
|
||||||
'adfs-enterprise',
|
|
||||||
'auth0',
|
|
||||||
'auth0-prod',
|
|
||||||
'auth0-dev',
|
|
||||||
'auth0-staging',
|
|
||||||
'onelogin',
|
|
||||||
'onelogin-prod',
|
|
||||||
'onelogin-corp',
|
|
||||||
'jumpcloud',
|
|
||||||
'jumpcloud-prod',
|
|
||||||
'jumpcloud-corp',
|
|
||||||
'ping-identity',
|
|
||||||
'ping-federate',
|
|
||||||
'pingone',
|
|
||||||
'shibboleth',
|
|
||||||
'shibboleth-idp',
|
|
||||||
'google-workspace',
|
|
||||||
'google-sso',
|
|
||||||
'saml',
|
|
||||||
'saml2',
|
|
||||||
'saml-sso',
|
|
||||||
'oidc',
|
|
||||||
'oidc-sso',
|
|
||||||
'openid-connect',
|
|
||||||
'custom-sso',
|
|
||||||
'enterprise-sso',
|
|
||||||
'company-sso',
|
|
||||||
]
|
|
||||||
|
|
||||||
interface SSOProvider {
|
interface SSOProvider {
|
||||||
id: string
|
id: string
|
||||||
providerId: string
|
providerId: string
|
||||||
@@ -565,7 +523,7 @@ export function SSO() {
|
|||||||
<Combobox
|
<Combobox
|
||||||
value={formData.providerId}
|
value={formData.providerId}
|
||||||
onChange={(value: string) => handleInputChange('providerId', value)}
|
onChange={(value: string) => handleInputChange('providerId', value)}
|
||||||
options={TRUSTED_SSO_PROVIDERS.map((id) => ({
|
options={SSO_TRUSTED_PROVIDERS.map((id) => ({
|
||||||
label: id,
|
label: id,
|
||||||
value: id,
|
value: id,
|
||||||
}))}
|
}))}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* List of trusted SSO provider identifiers.
|
||||||
|
* Used for validation and autocomplete in SSO configuration.
|
||||||
|
*/
|
||||||
export const SSO_TRUSTED_PROVIDERS = [
|
export const SSO_TRUSTED_PROVIDERS = [
|
||||||
'okta',
|
'okta',
|
||||||
'okta-saml',
|
'okta-saml',
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { organizationKeys } from '@/hooks/queries/organization'
|
import { organizationKeys } from '@/hooks/queries/organization'
|
||||||
|
|
||||||
@@ -75,39 +77,3 @@ export function useConfigureSSO() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete SSO provider mutation
|
|
||||||
*/
|
|
||||||
interface DeleteSSOParams {
|
|
||||||
providerId: string
|
|
||||||
orgId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDeleteSSO() {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async ({ providerId }: DeleteSSOParams) => {
|
|
||||||
const response = await fetch(`/api/auth/sso/providers/${providerId}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.message || 'Failed to delete SSO provider')
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
},
|
|
||||||
onSuccess: (_data, variables) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ssoKeys.providers() })
|
|
||||||
|
|
||||||
if (variables.orgId) {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: organizationKeys.detail(variables.orgId),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -185,16 +185,10 @@ export const HTTP = {
|
|||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
|
||||||
|
|
||||||
export const AGENT = {
|
export const AGENT = {
|
||||||
DEFAULT_MODEL: 'claude-sonnet-4-5',
|
DEFAULT_MODEL: 'claude-sonnet-4-5',
|
||||||
get DEFAULT_FUNCTION_TIMEOUT() {
|
DEFAULT_FUNCTION_TIMEOUT: 600000,
|
||||||
return getMaxExecutionTimeout()
|
REQUEST_TIMEOUT: 600000,
|
||||||
},
|
|
||||||
get REQUEST_TIMEOUT() {
|
|
||||||
return getMaxExecutionTimeout()
|
|
||||||
},
|
|
||||||
CUSTOM_TOOL_PREFIX: 'custom_',
|
CUSTOM_TOOL_PREFIX: 'custom_',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
hydrateUserFilesWithBase64,
|
hydrateUserFilesWithBase64,
|
||||||
} from '@/lib/uploads/utils/user-file-base64.server'
|
} from '@/lib/uploads/utils/user-file-base64.server'
|
||||||
import { sanitizeInputFormat, sanitizeTools } from '@/lib/workflows/comparison/normalize'
|
import { sanitizeInputFormat, sanitizeTools } from '@/lib/workflows/comparison/normalize'
|
||||||
|
import { validateBlockType } from '@/ee/access-control/utils/permission-check'
|
||||||
import {
|
import {
|
||||||
BlockType,
|
BlockType,
|
||||||
buildResumeApiUrl,
|
buildResumeApiUrl,
|
||||||
@@ -31,7 +32,6 @@ import { streamingResponseFormatProcessor } from '@/executor/utils'
|
|||||||
import { buildBlockExecutionError, normalizeError } from '@/executor/utils/errors'
|
import { buildBlockExecutionError, normalizeError } from '@/executor/utils/errors'
|
||||||
import { isJSONString } from '@/executor/utils/json'
|
import { isJSONString } from '@/executor/utils/json'
|
||||||
import { filterOutputForLog } from '@/executor/utils/output-filter'
|
import { filterOutputForLog } from '@/executor/utils/output-filter'
|
||||||
import { validateBlockType } from '@/executor/utils/permission-check'
|
|
||||||
import type { VariableResolver } from '@/executor/variables/resolver'
|
import type { VariableResolver } from '@/executor/variables/resolver'
|
||||||
import type { SerializedBlock } from '@/serializer/types'
|
import type { SerializedBlock } from '@/serializer/types'
|
||||||
import type { SubflowType } from '@/stores/workflows/workflow/types'
|
import type { SubflowType } from '@/stores/workflows/workflow/types'
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ import { createMcpToolId } from '@/lib/mcp/utils'
|
|||||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
import { getAllBlocks } from '@/blocks'
|
import { getAllBlocks } from '@/blocks'
|
||||||
import type { BlockOutput } from '@/blocks/types'
|
import type { BlockOutput } from '@/blocks/types'
|
||||||
|
import {
|
||||||
|
validateBlockType,
|
||||||
|
validateCustomToolsAllowed,
|
||||||
|
validateMcpToolsAllowed,
|
||||||
|
validateModelProvider,
|
||||||
|
} from '@/ee/access-control/utils/permission-check'
|
||||||
import { AGENT, BlockType, DEFAULTS, REFERENCE, stripCustomToolPrefix } from '@/executor/constants'
|
import { AGENT, BlockType, DEFAULTS, REFERENCE, stripCustomToolPrefix } from '@/executor/constants'
|
||||||
import { memoryService } from '@/executor/handlers/agent/memory'
|
import { memoryService } from '@/executor/handlers/agent/memory'
|
||||||
import type {
|
import type {
|
||||||
@@ -18,12 +24,6 @@ import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/execu
|
|||||||
import { collectBlockData } from '@/executor/utils/block-data'
|
import { collectBlockData } from '@/executor/utils/block-data'
|
||||||
import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http'
|
import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http'
|
||||||
import { stringifyJSON } from '@/executor/utils/json'
|
import { stringifyJSON } from '@/executor/utils/json'
|
||||||
import {
|
|
||||||
validateBlockType,
|
|
||||||
validateCustomToolsAllowed,
|
|
||||||
validateMcpToolsAllowed,
|
|
||||||
validateModelProvider,
|
|
||||||
} from '@/executor/utils/permission-check'
|
|
||||||
import { executeProviderRequest } from '@/providers'
|
import { executeProviderRequest } from '@/providers'
|
||||||
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
|
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
|
||||||
import type { SerializedBlock } from '@/serializer/types'
|
import type { SerializedBlock } from '@/serializer/types'
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
import type { BlockOutput } from '@/blocks/types'
|
import type { BlockOutput } from '@/blocks/types'
|
||||||
|
import { validateModelProvider } from '@/ee/access-control/utils/permission-check'
|
||||||
import { BlockType, DEFAULTS, EVALUATOR } from '@/executor/constants'
|
import { BlockType, DEFAULTS, EVALUATOR } from '@/executor/constants'
|
||||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||||
import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http'
|
import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http'
|
||||||
import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json'
|
import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json'
|
||||||
import { validateModelProvider } from '@/executor/utils/permission-check'
|
|
||||||
import { calculateCost, getProviderFromModel } from '@/providers/utils'
|
import { calculateCost, getProviderFromModel } from '@/providers/utils'
|
||||||
import type { SerializedBlock } from '@/serializer/types'
|
import type { SerializedBlock } from '@/serializer/types'
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
|
|||||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
import { generateRouterPrompt, generateRouterV2Prompt } from '@/blocks/blocks/router'
|
import { generateRouterPrompt, generateRouterV2Prompt } from '@/blocks/blocks/router'
|
||||||
import type { BlockOutput } from '@/blocks/types'
|
import type { BlockOutput } from '@/blocks/types'
|
||||||
|
import { validateModelProvider } from '@/ee/access-control/utils/permission-check'
|
||||||
import {
|
import {
|
||||||
BlockType,
|
BlockType,
|
||||||
DEFAULTS,
|
DEFAULTS,
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
} from '@/executor/constants'
|
} from '@/executor/constants'
|
||||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||||
import { buildAuthHeaders } from '@/executor/utils/http'
|
import { buildAuthHeaders } from '@/executor/utils/http'
|
||||||
import { validateModelProvider } from '@/executor/utils/permission-check'
|
|
||||||
import { calculateCost, getProviderFromModel } from '@/providers/utils'
|
import { calculateCost, getProviderFromModel } from '@/providers/utils'
|
||||||
import type { SerializedBlock } from '@/serializer/types'
|
import type { SerializedBlock } from '@/serializer/types'
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const sleep = async (ms: number, options: SleepOptions = {}): Promise<boolean> =
|
|||||||
const { signal, executionId } = options
|
const { signal, executionId } = options
|
||||||
const useRedis = isRedisCancellationEnabled() && !!executionId
|
const useRedis = isRedisCancellationEnabled() && !!executionId
|
||||||
|
|
||||||
if (signal?.aborted) {
|
if (!useRedis && signal?.aborted) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ const sleep = async (ms: number, options: SleepOptions = {}): Promise<boolean> =
|
|||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (mainTimeoutId) clearTimeout(mainTimeoutId)
|
if (mainTimeoutId) clearTimeout(mainTimeoutId)
|
||||||
if (checkIntervalId) clearInterval(checkIntervalId)
|
if (checkIntervalId) clearInterval(checkIntervalId)
|
||||||
if (signal) signal.removeEventListener('abort', onAbort)
|
if (!useRedis && signal) signal.removeEventListener('abort', onAbort)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onAbort = () => {
|
const onAbort = () => {
|
||||||
@@ -37,10 +37,6 @@ const sleep = async (ms: number, options: SleepOptions = {}): Promise<boolean> =
|
|||||||
resolve(false)
|
resolve(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signal) {
|
|
||||||
signal.addEventListener('abort', onAbort, { once: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useRedis) {
|
if (useRedis) {
|
||||||
checkIntervalId = setInterval(async () => {
|
checkIntervalId = setInterval(async () => {
|
||||||
if (resolved) return
|
if (resolved) return
|
||||||
@@ -53,6 +49,8 @@ const sleep = async (ms: number, options: SleepOptions = {}): Promise<boolean> =
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}, CANCELLATION_CHECK_INTERVAL_MS)
|
}, CANCELLATION_CHECK_INTERVAL_MS)
|
||||||
|
} else if (signal) {
|
||||||
|
signal.addEventListener('abort', onAbort, { once: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
mainTimeoutId = setTimeout(() => {
|
mainTimeoutId = setTimeout(() => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { fetchJson } from '@/hooks/selectors/helpers'
|
import { fetchJson } from '@/hooks/selectors/helpers'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||||
import { isAccessControlEnabled, isHosted } from '@/lib/core/config/feature-flags'
|
import { isAccessControlEnabled, isHosted } from '@/lib/core/config/feature-flags'
|
||||||
@@ -5,8 +7,8 @@ import {
|
|||||||
DEFAULT_PERMISSION_GROUP_CONFIG,
|
DEFAULT_PERMISSION_GROUP_CONFIG,
|
||||||
type PermissionGroupConfig,
|
type PermissionGroupConfig,
|
||||||
} from '@/lib/permission-groups/types'
|
} from '@/lib/permission-groups/types'
|
||||||
|
import { useUserPermissionConfig } from '@/ee/access-control/hooks/permission-groups'
|
||||||
import { useOrganizations } from '@/hooks/queries/organization'
|
import { useOrganizations } from '@/hooks/queries/organization'
|
||||||
import { useUserPermissionConfig } from '@/hooks/queries/permission-groups'
|
|
||||||
|
|
||||||
export interface PermissionConfigResult {
|
export interface PermissionConfigResult {
|
||||||
config: PermissionGroupConfig
|
config: PermissionGroupConfig
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
export { AGENT_CARD_PATH } from '@a2a-js/sdk'
|
export { AGENT_CARD_PATH } from '@a2a-js/sdk'
|
||||||
|
|
||||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
|
||||||
|
|
||||||
export const A2A_PROTOCOL_VERSION = '0.3.0'
|
export const A2A_PROTOCOL_VERSION = '0.3.0'
|
||||||
|
|
||||||
export const A2A_DEFAULT_TIMEOUT = DEFAULT_EXECUTION_TIMEOUT_MS
|
export const A2A_DEFAULT_TIMEOUT = 300000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum number of messages stored per task in the database.
|
* Maximum number of messages stored per task in the database.
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ import { sendEmail } from '@/lib/messaging/email/mailer'
|
|||||||
import { getFromEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils'
|
import { getFromEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils'
|
||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||||
|
import { SSO_TRUSTED_PROVIDERS } from '@/ee/sso/constants'
|
||||||
import { createAnonymousSession, ensureAnonymousUserExists } from './anonymous'
|
import { createAnonymousSession, ensureAnonymousUserExists } from './anonymous'
|
||||||
import { SSO_TRUSTED_PROVIDERS } from './sso/constants'
|
|
||||||
|
|
||||||
const logger = createLogger('Auth')
|
const logger = createLogger('Auth')
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { and, eq, isNull } from 'drizzle-orm'
|
|||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||||
import { isHiddenFromDisplay } from '@/blocks/types'
|
import { isHiddenFromDisplay } from '@/blocks/types'
|
||||||
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
import { escapeRegExp } from '@/executor/constants'
|
import { escapeRegExp } from '@/executor/constants'
|
||||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
|
||||||
import type { ChatContext } from '@/stores/panel/copilot/types'
|
import type { ChatContext } from '@/stores/panel/copilot/types'
|
||||||
|
|
||||||
export type AgentContextType =
|
export type AgentContextType =
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import type { ToolUIConfig } from './ui-config'
|
|||||||
|
|
||||||
const baseToolLogger = createLogger('BaseClientTool')
|
const baseToolLogger = createLogger('BaseClientTool')
|
||||||
|
|
||||||
const DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1000
|
/** Default timeout for tool execution (5 minutes) */
|
||||||
|
const DEFAULT_TOOL_TIMEOUT_MS = 2 * 60 * 1000
|
||||||
|
|
||||||
export const WORKFLOW_EXECUTION_TIMEOUT_MS = 5 * 60 * 1000
|
/** Timeout for tools that run workflows (10 minutes) */
|
||||||
|
export const WORKFLOW_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000
|
||||||
|
|
||||||
// Client tool call states used by the new runtime
|
// Client tool call states used by the new runtime
|
||||||
export enum ClientToolCallState {
|
export enum ClientToolCallState {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@/lib/copilot/tools/shared/schemas'
|
} from '@/lib/copilot/tools/shared/schemas'
|
||||||
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
|
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
|
||||||
import { isHiddenFromDisplay, type SubBlockConfig } from '@/blocks/types'
|
import { isHiddenFromDisplay, type SubBlockConfig } from '@/blocks/types'
|
||||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
||||||
import { tools as toolsRegistry } from '@/tools/registry'
|
import { tools as toolsRegistry } from '@/tools/registry'
|
||||||
import { getTrigger, isTriggerValid } from '@/triggers'
|
import { getTrigger, isTriggerValid } from '@/triggers'
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
type GetBlockOptionsResultType,
|
type GetBlockOptionsResultType,
|
||||||
} from '@/lib/copilot/tools/shared/schemas'
|
} from '@/lib/copilot/tools/shared/schemas'
|
||||||
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
|
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
|
||||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
import { tools as toolsRegistry } from '@/tools/registry'
|
import { tools as toolsRegistry } from '@/tools/registry'
|
||||||
|
|
||||||
export const getBlockOptionsServerTool: BaseServerTool<
|
export const getBlockOptionsServerTool: BaseServerTool<
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from '@/lib/copilot/tools/shared/schemas'
|
} from '@/lib/copilot/tools/shared/schemas'
|
||||||
import { registry as blockRegistry } from '@/blocks/registry'
|
import { registry as blockRegistry } from '@/blocks/registry'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
|
|
||||||
export const getBlocksAndToolsServerTool: BaseServerTool<
|
export const getBlocksAndToolsServerTool: BaseServerTool<
|
||||||
ReturnType<typeof GetBlocksAndToolsInput.parse>,
|
ReturnType<typeof GetBlocksAndToolsInput.parse>,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@/lib/copilot/tools/shared/schemas'
|
} from '@/lib/copilot/tools/shared/schemas'
|
||||||
import { registry as blockRegistry } from '@/blocks/registry'
|
import { registry as blockRegistry } from '@/blocks/registry'
|
||||||
import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types'
|
import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types'
|
||||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
||||||
import { tools as toolsRegistry } from '@/tools/registry'
|
import { tools as toolsRegistry } from '@/tools/registry'
|
||||||
import { getTrigger, isTriggerValid } from '@/triggers'
|
import { getTrigger, isTriggerValid } from '@/triggers'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { z } from 'zod'
|
|||||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||||
import { registry as blockRegistry } from '@/blocks/registry'
|
import { registry as blockRegistry } from '@/blocks/registry'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
|
|
||||||
export const GetTriggerBlocksInput = z.object({})
|
export const GetTriggerBlocksInput = z.object({})
|
||||||
export const GetTriggerBlocksResult = z.object({
|
export const GetTriggerBlocksResult = z.object({
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import { buildCanonicalIndex, isCanonicalPair } from '@/lib/workflows/subblocks/
|
|||||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||||
import { getAllBlocks, getBlock } from '@/blocks/registry'
|
import { getAllBlocks, getBlock } from '@/blocks/registry'
|
||||||
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
|
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
|
||||||
|
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
|
||||||
import { EDGE, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
import { EDGE, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
||||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
|
||||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||||
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
|
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
|
||||||
|
|
||||||
|
|||||||
@@ -170,11 +170,6 @@ export const env = createEnv({
|
|||||||
RATE_LIMIT_ENTERPRISE_SYNC: z.string().optional().default('600'), // Enterprise tier sync API executions per minute
|
RATE_LIMIT_ENTERPRISE_SYNC: z.string().optional().default('600'), // Enterprise tier sync API executions per minute
|
||||||
RATE_LIMIT_ENTERPRISE_ASYNC: z.string().optional().default('5000'), // Enterprise tier async API executions per minute
|
RATE_LIMIT_ENTERPRISE_ASYNC: z.string().optional().default('5000'), // Enterprise tier async API executions per minute
|
||||||
|
|
||||||
EXECUTION_TIMEOUT_FREE: z.string().optional().default('300'),
|
|
||||||
EXECUTION_TIMEOUT_PRO: z.string().optional().default('3600'),
|
|
||||||
EXECUTION_TIMEOUT_TEAM: z.string().optional().default('3600'),
|
|
||||||
EXECUTION_TIMEOUT_ENTERPRISE: z.string().optional().default('3600'),
|
|
||||||
|
|
||||||
// Knowledge Base Processing Configuration - Shared across all processing methods
|
// Knowledge Base Processing Configuration - Shared across all processing methods
|
||||||
KB_CONFIG_MAX_DURATION: z.number().optional().default(600), // Max processing duration in seconds (10 minutes)
|
KB_CONFIG_MAX_DURATION: z.number().optional().default(600), // Max processing duration in seconds (10 minutes)
|
||||||
KB_CONFIG_MAX_ATTEMPTS: z.number().optional().default(3), // Max retry attempts
|
KB_CONFIG_MAX_ATTEMPTS: z.number().optional().default(3), // Max retry attempts
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './types'
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import { env } from '@/lib/core/config/env'
|
|
||||||
import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types'
|
|
||||||
|
|
||||||
export interface ExecutionTimeoutConfig {
|
|
||||||
sync: number
|
|
||||||
async: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_SYNC_TIMEOUTS = {
|
|
||||||
free: 300,
|
|
||||||
pro: 3600,
|
|
||||||
team: 3600,
|
|
||||||
enterprise: 3600,
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const ASYNC_TIMEOUT_SECONDS = 5400
|
|
||||||
|
|
||||||
function getSyncTimeoutForPlan(plan: SubscriptionPlan): number {
|
|
||||||
const envVarMap: Record<SubscriptionPlan, string | undefined> = {
|
|
||||||
free: env.EXECUTION_TIMEOUT_FREE,
|
|
||||||
pro: env.EXECUTION_TIMEOUT_PRO,
|
|
||||||
team: env.EXECUTION_TIMEOUT_TEAM,
|
|
||||||
enterprise: env.EXECUTION_TIMEOUT_ENTERPRISE,
|
|
||||||
}
|
|
||||||
return (Number.parseInt(envVarMap[plan] || '') || DEFAULT_SYNC_TIMEOUTS[plan]) * 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EXECUTION_TIMEOUTS: Record<SubscriptionPlan, ExecutionTimeoutConfig> = {
|
|
||||||
free: {
|
|
||||||
sync: getSyncTimeoutForPlan('free'),
|
|
||||||
async: ASYNC_TIMEOUT_SECONDS * 1000,
|
|
||||||
},
|
|
||||||
pro: {
|
|
||||||
sync: getSyncTimeoutForPlan('pro'),
|
|
||||||
async: ASYNC_TIMEOUT_SECONDS * 1000,
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
sync: getSyncTimeoutForPlan('team'),
|
|
||||||
async: ASYNC_TIMEOUT_SECONDS * 1000,
|
|
||||||
},
|
|
||||||
enterprise: {
|
|
||||||
sync: getSyncTimeoutForPlan('enterprise'),
|
|
||||||
async: ASYNC_TIMEOUT_SECONDS * 1000,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getExecutionTimeout(
|
|
||||||
plan: SubscriptionPlan | undefined,
|
|
||||||
type: 'sync' | 'async' = 'sync'
|
|
||||||
): number {
|
|
||||||
return EXECUTION_TIMEOUTS[plan || 'free'][type]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getExecutionTimeoutSeconds(
|
|
||||||
plan: SubscriptionPlan | undefined,
|
|
||||||
type: 'sync' | 'async' = 'sync'
|
|
||||||
): number {
|
|
||||||
return Math.floor(getExecutionTimeout(plan, type) / 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMaxExecutionTimeout(): number {
|
|
||||||
return EXECUTION_TIMEOUTS.enterprise.async
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_EXECUTION_TIMEOUT_MS = EXECUTION_TIMEOUTS.free.sync
|
|
||||||
|
|
||||||
export class ExecutionTimeoutError extends Error {
|
|
||||||
constructor(
|
|
||||||
public readonly timeoutMs: number,
|
|
||||||
public readonly plan?: SubscriptionPlan
|
|
||||||
) {
|
|
||||||
const timeoutSeconds = Math.floor(timeoutMs / 1000)
|
|
||||||
const timeoutMinutes = Math.floor(timeoutSeconds / 60)
|
|
||||||
const displayTime =
|
|
||||||
timeoutMinutes > 0
|
|
||||||
? `${timeoutMinutes} minute${timeoutMinutes > 1 ? 's' : ''}`
|
|
||||||
: `${timeoutSeconds} seconds`
|
|
||||||
super(`Execution timed out after ${displayTime}`)
|
|
||||||
this.name = 'ExecutionTimeoutError'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isTimeoutError(error: unknown): boolean {
|
|
||||||
if (error instanceof ExecutionTimeoutError) return true
|
|
||||||
if (!(error instanceof Error)) return false
|
|
||||||
|
|
||||||
const name = error.name.toLowerCase()
|
|
||||||
const message = error.message.toLowerCase()
|
|
||||||
|
|
||||||
return (
|
|
||||||
name === 'timeouterror' ||
|
|
||||||
name === 'aborterror' ||
|
|
||||||
message.includes('timeout') ||
|
|
||||||
message.includes('timed out') ||
|
|
||||||
message.includes('aborted')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createTimeoutError(
|
|
||||||
timeoutMs: number,
|
|
||||||
plan?: SubscriptionPlan
|
|
||||||
): ExecutionTimeoutError {
|
|
||||||
return new ExecutionTimeoutError(timeoutMs, plan)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTimeoutErrorMessage(error: unknown, timeoutMs?: number): string {
|
|
||||||
if (error instanceof ExecutionTimeoutError) {
|
|
||||||
return error.message
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeoutMs) {
|
|
||||||
const timeoutSeconds = Math.floor(timeoutMs / 1000)
|
|
||||||
const timeoutMinutes = Math.floor(timeoutSeconds / 60)
|
|
||||||
const displayTime =
|
|
||||||
timeoutMinutes > 0
|
|
||||||
? `${timeoutMinutes} minute${timeoutMinutes > 1 ? 's' : ''}`
|
|
||||||
: `${timeoutSeconds} seconds`
|
|
||||||
return `Execution timed out after ${displayTime}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Execution timed out'
|
|
||||||
}
|
|
||||||
@@ -153,22 +153,50 @@ export function formatCompactTimestamp(iso: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a duration in milliseconds to a human-readable format
|
* Format a duration to a human-readable format
|
||||||
* @param durationMs - The duration in milliseconds
|
* @param duration - Duration in milliseconds (number) or as string (e.g., "500ms")
|
||||||
* @param options - Optional formatting options
|
* @param options - Optional formatting options
|
||||||
* @param options.precision - Number of decimal places for seconds (default: 0)
|
* @param options.precision - Number of decimal places for seconds (default: 0), trailing zeros are stripped
|
||||||
* @returns A formatted duration string
|
* @returns A formatted duration string, or null if input is null/undefined
|
||||||
*/
|
*/
|
||||||
export function formatDuration(durationMs: number, options?: { precision?: number }): string {
|
export function formatDuration(
|
||||||
const precision = options?.precision ?? 0
|
duration: number | string | undefined | null,
|
||||||
|
options?: { precision?: number }
|
||||||
if (durationMs < 1000) {
|
): string | null {
|
||||||
return `${durationMs}ms`
|
if (duration === undefined || duration === null) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const seconds = durationMs / 1000
|
// Parse string durations (e.g., "500ms", "0.44ms", "1234")
|
||||||
|
let ms: number
|
||||||
|
if (typeof duration === 'string') {
|
||||||
|
ms = Number.parseFloat(duration.replace(/[^0-9.-]/g, ''))
|
||||||
|
if (!Number.isFinite(ms)) {
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ms = duration
|
||||||
|
}
|
||||||
|
|
||||||
|
const precision = options?.precision ?? 0
|
||||||
|
|
||||||
|
if (ms < 1) {
|
||||||
|
// Sub-millisecond: show with 2 decimal places
|
||||||
|
return `${ms.toFixed(2)}ms`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ms < 1000) {
|
||||||
|
// Milliseconds: round to integer
|
||||||
|
return `${Math.round(ms)}ms`
|
||||||
|
}
|
||||||
|
|
||||||
|
const seconds = ms / 1000
|
||||||
if (seconds < 60) {
|
if (seconds < 60) {
|
||||||
return precision > 0 ? `${seconds.toFixed(precision)}s` : `${Math.floor(seconds)}s`
|
if (precision > 0) {
|
||||||
|
// Strip trailing zeros (e.g., "5.00s" -> "5s", "5.10s" -> "5.1s")
|
||||||
|
return `${seconds.toFixed(precision).replace(/\.?0+$/, '')}s`
|
||||||
|
}
|
||||||
|
return `${Math.floor(seconds)}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
const minutes = Math.floor(seconds / 60)
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
/**
|
||||||
|
* Execution timeout constants
|
||||||
|
*
|
||||||
|
* DEFAULT_EXECUTION_TIMEOUT_MS: The default timeout for executing user code (10 minutes)
|
||||||
|
*/
|
||||||
|
|
||||||
export { DEFAULT_EXECUTION_TIMEOUT_MS }
|
export const DEFAULT_EXECUTION_TIMEOUT_MS = 600000 // 10 minutes (600 seconds)
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { checkServerSideUsageLimits } from '@/lib/billing/calculations/usage-monitor'
|
import { checkServerSideUsageLimits } from '@/lib/billing/calculations/usage-monitor'
|
||||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
||||||
import { getExecutionTimeout } from '@/lib/core/execution-limits'
|
|
||||||
import { RateLimiter } from '@/lib/core/rate-limiter/rate-limiter'
|
import { RateLimiter } from '@/lib/core/rate-limiter/rate-limiter'
|
||||||
import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types'
|
|
||||||
import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
||||||
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
|
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
|
||||||
import type { CoreTriggerType } from '@/stores/logs/filters/types'
|
import type { CoreTriggerType } from '@/stores/logs/filters/types'
|
||||||
@@ -135,10 +133,10 @@ export interface PreprocessExecutionResult {
|
|||||||
success: boolean
|
success: boolean
|
||||||
error?: {
|
error?: {
|
||||||
message: string
|
message: string
|
||||||
statusCode: number
|
statusCode: number // HTTP status code (401, 402, 403, 404, 429, 500)
|
||||||
logCreated: boolean
|
logCreated: boolean // Whether error was logged to execution_logs
|
||||||
}
|
}
|
||||||
actorUserId?: string
|
actorUserId?: string // The user ID that will be billed
|
||||||
workflowRecord?: WorkflowRecord
|
workflowRecord?: WorkflowRecord
|
||||||
userSubscription?: SubscriptionInfo | null
|
userSubscription?: SubscriptionInfo | null
|
||||||
rateLimitInfo?: {
|
rateLimitInfo?: {
|
||||||
@@ -146,10 +144,6 @@ export interface PreprocessExecutionResult {
|
|||||||
remaining: number
|
remaining: number
|
||||||
resetAt: Date
|
resetAt: Date
|
||||||
}
|
}
|
||||||
executionTimeout?: {
|
|
||||||
sync: number
|
|
||||||
async: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowRecord = typeof workflow.$inferSelect
|
type WorkflowRecord = typeof workflow.$inferSelect
|
||||||
@@ -490,17 +484,12 @@ export async function preprocessExecution(
|
|||||||
triggerType,
|
triggerType,
|
||||||
})
|
})
|
||||||
|
|
||||||
const plan = userSubscription?.plan as SubscriptionPlan | undefined
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
actorUserId,
|
actorUserId,
|
||||||
workflowRecord,
|
workflowRecord,
|
||||||
userSubscription,
|
userSubscription,
|
||||||
rateLimitInfo,
|
rateLimitInfo,
|
||||||
executionTimeout: {
|
|
||||||
sync: getExecutionTimeout(plan, 'sync'),
|
|
||||||
async: getExecutionTimeout(plan, 'async'),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -776,16 +776,11 @@ export class LoggingSession {
|
|||||||
await db
|
await db
|
||||||
.update(workflowExecutionLogs)
|
.update(workflowExecutionLogs)
|
||||||
.set({
|
.set({
|
||||||
level: 'error',
|
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
executionData: sql`jsonb_set(
|
executionData: sql`jsonb_set(
|
||||||
jsonb_set(
|
COALESCE(execution_data, '{}'::jsonb),
|
||||||
COALESCE(execution_data, '{}'::jsonb),
|
ARRAY['error'],
|
||||||
ARRAY['error'],
|
to_jsonb(${message}::text)
|
||||||
to_jsonb(${message}::text)
|
|
||||||
),
|
|
||||||
ARRAY['finalOutput'],
|
|
||||||
jsonb_build_object('error', ${message}::text)
|
|
||||||
)`,
|
)`,
|
||||||
})
|
})
|
||||||
.where(eq(workflowExecutionLogs.executionId, executionId))
|
.where(eq(workflowExecutionLogs.executionId, executionId))
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|||||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||||
import type { ListToolsResult, Tool } from '@modelcontextprotocol/sdk/types.js'
|
import type { ListToolsResult, Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
|
||||||
import {
|
import {
|
||||||
McpConnectionError,
|
McpConnectionError,
|
||||||
type McpConnectionStatus,
|
type McpConnectionStatus,
|
||||||
@@ -203,7 +202,7 @@ export class McpClient {
|
|||||||
const sdkResult = await this.client.callTool(
|
const sdkResult = await this.client.callTool(
|
||||||
{ name: toolCall.name, arguments: toolCall.arguments },
|
{ name: toolCall.name, arguments: toolCall.arguments },
|
||||||
undefined,
|
undefined,
|
||||||
{ timeout: getMaxExecutionTimeout() }
|
{ timeout: 600000 } // 10 minutes - override SDK's 60s default
|
||||||
)
|
)
|
||||||
|
|
||||||
return sdkResult as McpToolResult
|
return sdkResult as McpToolResult
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function sanitizeHeaders(
|
|||||||
* Client-safe MCP constants
|
* Client-safe MCP constants
|
||||||
*/
|
*/
|
||||||
export const MCP_CLIENT_CONSTANTS = {
|
export const MCP_CLIENT_CONSTANTS = {
|
||||||
CLIENT_TIMEOUT: 5 * 60 * 1000,
|
CLIENT_TIMEOUT: 600000,
|
||||||
MAX_RETRIES: 3,
|
MAX_RETRIES: 3,
|
||||||
RECONNECT_DELAY: 1000,
|
RECONNECT_DELAY: 1000,
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ describe('generateMcpServerId', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('MCP_CONSTANTS', () => {
|
describe('MCP_CONSTANTS', () => {
|
||||||
it.concurrent('has correct execution timeout (5 minutes)', () => {
|
it.concurrent('has correct execution timeout (10 minutes)', () => {
|
||||||
expect(MCP_CONSTANTS.EXECUTION_TIMEOUT).toBe(300000)
|
expect(MCP_CONSTANTS.EXECUTION_TIMEOUT).toBe(600000)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('has correct cache timeout (5 minutes)', () => {
|
it.concurrent('has correct cache timeout (5 minutes)', () => {
|
||||||
@@ -107,8 +107,8 @@ describe('MCP_CONSTANTS', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('MCP_CLIENT_CONSTANTS', () => {
|
describe('MCP_CLIENT_CONSTANTS', () => {
|
||||||
it.concurrent('has correct client timeout (5 minutes)', () => {
|
it.concurrent('has correct client timeout (10 minutes)', () => {
|
||||||
expect(MCP_CLIENT_CONSTANTS.CLIENT_TIMEOUT).toBe(300000)
|
expect(MCP_CLIENT_CONSTANTS.CLIENT_TIMEOUT).toBe(600000)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('has correct auto refresh interval (5 minutes)', () => {
|
it.concurrent('has correct auto refresh interval (5 minutes)', () => {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { DEFAULT_EXECUTION_TIMEOUT_MS, getExecutionTimeout } from '@/lib/core/execution-limits'
|
|
||||||
import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types'
|
|
||||||
import type { McpApiResponse } from '@/lib/mcp/types'
|
import type { McpApiResponse } from '@/lib/mcp/types'
|
||||||
import { isMcpTool, MCP } from '@/executor/constants'
|
import { isMcpTool, MCP } from '@/executor/constants'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP-specific constants
|
||||||
|
*/
|
||||||
export const MCP_CONSTANTS = {
|
export const MCP_CONSTANTS = {
|
||||||
EXECUTION_TIMEOUT: DEFAULT_EXECUTION_TIMEOUT_MS,
|
EXECUTION_TIMEOUT: 600000,
|
||||||
CACHE_TIMEOUT: 5 * 60 * 1000,
|
CACHE_TIMEOUT: 5 * 60 * 1000,
|
||||||
DEFAULT_RETRIES: 3,
|
DEFAULT_RETRIES: 3,
|
||||||
DEFAULT_CONNECTION_TIMEOUT: 30000,
|
DEFAULT_CONNECTION_TIMEOUT: 30000,
|
||||||
@@ -13,10 +14,6 @@ export const MCP_CONSTANTS = {
|
|||||||
MAX_CONSECUTIVE_FAILURES: 3,
|
MAX_CONSECUTIVE_FAILURES: 3,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export function getMcpExecutionTimeout(plan?: SubscriptionPlan): number {
|
|
||||||
return getExecutionTimeout(plan, 'sync')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core MCP tool parameter keys that are metadata, not user-entered test values.
|
* Core MCP tool parameter keys that are metadata, not user-entered test values.
|
||||||
* These should be preserved when cleaning up params during schema updates.
|
* These should be preserved when cleaning up params during schema updates.
|
||||||
@@ -48,8 +45,11 @@ export function sanitizeHeaders(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client-safe MCP constants
|
||||||
|
*/
|
||||||
export const MCP_CLIENT_CONSTANTS = {
|
export const MCP_CLIENT_CONSTANTS = {
|
||||||
CLIENT_TIMEOUT: DEFAULT_EXECUTION_TIMEOUT_MS,
|
CLIENT_TIMEOUT: 600000,
|
||||||
AUTO_REFRESH_INTERVAL: 5 * 60 * 1000,
|
AUTO_REFRESH_INTERVAL: 5 * 60 * 1000,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ export interface ExecutionErrorEvent extends BaseExecutionEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execution cancelled event
|
||||||
|
*/
|
||||||
export interface ExecutionCancelledEvent extends BaseExecutionEvent {
|
export interface ExecutionCancelledEvent extends BaseExecutionEvent {
|
||||||
type: 'execution:cancelled'
|
type: 'execution:cancelled'
|
||||||
workflowId: string
|
workflowId: string
|
||||||
@@ -168,6 +171,9 @@ export type ExecutionEvent =
|
|||||||
| StreamChunkEvent
|
| StreamChunkEvent
|
||||||
| StreamDoneEvent
|
| StreamDoneEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracted data types for use in callbacks
|
||||||
|
*/
|
||||||
export type ExecutionStartedData = ExecutionStartedEvent['data']
|
export type ExecutionStartedData = ExecutionStartedEvent['data']
|
||||||
export type ExecutionCompletedData = ExecutionCompletedEvent['data']
|
export type ExecutionCompletedData = ExecutionCompletedEvent['data']
|
||||||
export type ExecutionErrorData = ExecutionErrorEvent['data']
|
export type ExecutionErrorData = ExecutionErrorEvent['data']
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
|
||||||
import type { RunActorParams, RunActorResult } from '@/tools/apify/types'
|
import type { RunActorParams, RunActorResult } from '@/tools/apify/types'
|
||||||
import type { ToolConfig } from '@/tools/types'
|
import type { ToolConfig } from '@/tools/types'
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 5000
|
const POLL_INTERVAL_MS = 5000 // 5 seconds between polls
|
||||||
const MAX_POLL_TIME_MS = DEFAULT_EXECUTION_TIMEOUT_MS
|
const MAX_POLL_TIME_MS = 300000 // 5 minutes maximum polling time
|
||||||
|
|
||||||
export const apifyRunActorAsyncTool: ToolConfig<RunActorParams, RunActorResult> = {
|
export const apifyRunActorAsyncTool: ToolConfig<RunActorParams, RunActorResult> = {
|
||||||
id: 'apify_run_actor_async',
|
id: 'apify_run_actor_async',
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
|
||||||
import type { BrowserUseRunTaskParams, BrowserUseRunTaskResponse } from '@/tools/browser_use/types'
|
import type { BrowserUseRunTaskParams, BrowserUseRunTaskResponse } from '@/tools/browser_use/types'
|
||||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
const logger = createLogger('BrowserUseTool')
|
const logger = createLogger('BrowserUseTool')
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 5000
|
const POLL_INTERVAL_MS = 5000
|
||||||
const MAX_POLL_TIME_MS = DEFAULT_EXECUTION_TIMEOUT_MS
|
const MAX_POLL_TIME_MS = 600000 // 10 minutes
|
||||||
const MAX_CONSECUTIVE_ERRORS = 3
|
const MAX_CONSECUTIVE_ERRORS = 3
|
||||||
|
|
||||||
async function createSessionWithProfile(
|
async function createSessionWithProfile(
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
|
||||||
import type { ExaResearchParams, ExaResearchResponse } from '@/tools/exa/types'
|
import type { ExaResearchParams, ExaResearchResponse } from '@/tools/exa/types'
|
||||||
import type { ToolConfig } from '@/tools/types'
|
import type { ToolConfig } from '@/tools/types'
|
||||||
|
|
||||||
const logger = createLogger('ExaResearchTool')
|
const logger = createLogger('ExaResearchTool')
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 5000
|
const POLL_INTERVAL_MS = 5000 // 5 seconds between polls
|
||||||
const MAX_POLL_TIME_MS = DEFAULT_EXECUTION_TIMEOUT_MS
|
const MAX_POLL_TIME_MS = 300000 // 5 minutes maximum polling time
|
||||||
|
|
||||||
export const researchTool: ToolConfig<ExaResearchParams, ExaResearchResponse> = {
|
export const researchTool: ToolConfig<ExaResearchParams, ExaResearchResponse> = {
|
||||||
id: 'exa_research',
|
id: 'exa_research',
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
|
||||||
import type { AgentParams, AgentResponse } from '@/tools/firecrawl/types'
|
import type { AgentParams, AgentResponse } from '@/tools/firecrawl/types'
|
||||||
import type { ToolConfig } from '@/tools/types'
|
import type { ToolConfig } from '@/tools/types'
|
||||||
|
|
||||||
const logger = createLogger('FirecrawlAgentTool')
|
const logger = createLogger('FirecrawlAgentTool')
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 5000
|
const POLL_INTERVAL_MS = 5000 // 5 seconds between polls
|
||||||
const MAX_POLL_TIME_MS = DEFAULT_EXECUTION_TIMEOUT_MS
|
const MAX_POLL_TIME_MS = 300000 // 5 minutes maximum polling time
|
||||||
|
|
||||||
export const agentTool: ToolConfig<AgentParams, AgentResponse> = {
|
export const agentTool: ToolConfig<AgentParams, AgentResponse> = {
|
||||||
id: 'firecrawl_agent',
|
id: 'firecrawl_agent',
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
|
||||||
import type { FirecrawlCrawlParams, FirecrawlCrawlResponse } from '@/tools/firecrawl/types'
|
import type { FirecrawlCrawlParams, FirecrawlCrawlResponse } from '@/tools/firecrawl/types'
|
||||||
import { CRAWLED_PAGE_OUTPUT_PROPERTIES } from '@/tools/firecrawl/types'
|
import { CRAWLED_PAGE_OUTPUT_PROPERTIES } from '@/tools/firecrawl/types'
|
||||||
import type { ToolConfig } from '@/tools/types'
|
import type { ToolConfig } from '@/tools/types'
|
||||||
|
|
||||||
const logger = createLogger('FirecrawlCrawlTool')
|
const logger = createLogger('FirecrawlCrawlTool')
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 5000
|
const POLL_INTERVAL_MS = 5000 // 5 seconds between polls
|
||||||
const MAX_POLL_TIME_MS = DEFAULT_EXECUTION_TIMEOUT_MS
|
const MAX_POLL_TIME_MS = 300000 // 5 minutes maximum polling time
|
||||||
|
|
||||||
export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse> = {
|
export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse> = {
|
||||||
id: 'firecrawl_crawl',
|
id: 'firecrawl_crawl',
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
|
||||||
import type { ExtractParams, ExtractResponse } from '@/tools/firecrawl/types'
|
import type { ExtractParams, ExtractResponse } from '@/tools/firecrawl/types'
|
||||||
import type { ToolConfig } from '@/tools/types'
|
import type { ToolConfig } from '@/tools/types'
|
||||||
|
|
||||||
const logger = createLogger('FirecrawlExtractTool')
|
const logger = createLogger('FirecrawlExtractTool')
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 5000
|
const POLL_INTERVAL_MS = 5000 // 5 seconds between polls
|
||||||
const MAX_POLL_TIME_MS = DEFAULT_EXECUTION_TIMEOUT_MS
|
const MAX_POLL_TIME_MS = 300000 // 5 minutes maximum polling time
|
||||||
|
|
||||||
export const extractTool: ToolConfig<ExtractParams, ExtractResponse> = {
|
export const extractTool: ToolConfig<ExtractParams, ExtractResponse> = {
|
||||||
id: 'firecrawl_extract',
|
id: 'firecrawl_extract',
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { generateInternalToken } from '@/lib/auth/internal'
|
import { generateInternalToken } from '@/lib/auth/internal'
|
||||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
|
||||||
import { secureFetchWithPinnedIP, validateUrlWithDNS } from '@/lib/core/security/input-validation'
|
import { secureFetchWithPinnedIP, validateUrlWithDNS } from '@/lib/core/security/input-validation'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
@@ -626,8 +625,9 @@ async function executeToolRequest(
|
|||||||
let response: Response
|
let response: Response
|
||||||
|
|
||||||
if (isInternalRoute) {
|
if (isInternalRoute) {
|
||||||
|
// Set up AbortController for timeout support on internal routes
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeout = requestParams.timeout || DEFAULT_EXECUTION_TIMEOUT_MS
|
const timeout = requestParams.timeout || 300000
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { AGENT, isCustomTool } from '@/executor/constants'
|
import { AGENT, isCustomTool } from '@/executor/constants'
|
||||||
import { getCustomTool } from '@/hooks/queries/custom-tools'
|
import { getCustomTool } from '@/hooks/queries/custom-tools'
|
||||||
@@ -124,7 +123,9 @@ export function formatRequestParams(tool: ToolConfig, params: Record<string, any
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TIMEOUT_MS = getMaxExecutionTimeout()
|
// Get timeout from params (if specified) and validate
|
||||||
|
// Must be a finite positive number, max 600000ms (10 minutes) as documented
|
||||||
|
const MAX_TIMEOUT_MS = 600000
|
||||||
const rawTimeout = params.timeout
|
const rawTimeout = params.timeout
|
||||||
const timeout = rawTimeout != null ? Number(rawTimeout) : undefined
|
const timeout = rawTimeout != null ? Number(rawTimeout) : undefined
|
||||||
const validTimeout =
|
const validTimeout =
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
|||||||
project: env.TRIGGER_PROJECT_ID!,
|
project: env.TRIGGER_PROJECT_ID!,
|
||||||
runtime: 'node',
|
runtime: 'node',
|
||||||
logLevel: 'log',
|
logLevel: 'log',
|
||||||
maxDuration: 5400,
|
maxDuration: 600,
|
||||||
retries: {
|
retries: {
|
||||||
enabledInDev: false,
|
enabledInDev: false,
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
Reference in New Issue
Block a user