From d6ec115348d0581fc2e6729298db7f31c776d1d6 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 7 Apr 2026 16:11:31 -0700 Subject: [PATCH 1/3] v0.6.29: login improvements, posthog telemetry (#4026) * feat(posthog): Add tracking on mothership abort (#4023) Co-authored-by: Theodore Li * fix(login): fix captcha headers for manual login (#4025) * fix(signup): fix turnstile key loading * fix(login): fix captcha header passing * Catch user already exists, remove login form captcha --- apps/sim/app/(auth)/signup/signup-form.tsx | 11 +++-------- .../app/workspace/[workspaceId]/home/home.tsx | 12 ++++++++++-- .../w/[workflowId]/components/panel/panel.tsx | 19 ++++++++++++++++++- apps/sim/lib/posthog/events.ts | 5 +++++ 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index 55a0508ec1b..afb27cd729a 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -270,10 +270,8 @@ function SignupFormContent({ name: sanitizedName, }, { - fetchOptions: { - headers: { - ...(token ? { 'x-captcha-response': token } : {}), - }, + headers: { + ...(token ? { 'x-captcha-response': token } : {}), }, onError: (ctx) => { logger.error('Signup error:', ctx.error) @@ -282,10 +280,7 @@ function SignupFormContent({ let errorCode = 'unknown' if (ctx.error.code?.includes('USER_ALREADY_EXISTS')) { errorCode = 'user_already_exists' - errorMessage.push( - 'An account with this email already exists. Please sign in instead.' - ) - setEmailError(errorMessage[0]) + setEmailError('An account with this email already exists. Please sign in instead.') } else if ( ctx.error.code?.includes('BAD_REQUEST') || ctx.error.message?.includes('Email and password sign up is not enabled') diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index d76f17ff454..38367339197 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -223,6 +223,14 @@ export function Home({ chatId }: HomeProps = {}) { posthogRef.current = posthog }, [posthog]) + const handleStopGeneration = useCallback(() => { + captureEvent(posthogRef.current, 'task_generation_aborted', { + workspace_id: workspaceId, + view: 'mothership', + }) + stopGeneration() + }, [stopGeneration, workspaceId]) + const handleSubmit = useCallback( (text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => { const trimmed = text.trim() @@ -334,7 +342,7 @@ export function Home({ chatId }: HomeProps = {}) { defaultValue={initialPrompt} onSubmit={handleSubmit} isSending={isSending} - onStopGeneration={stopGeneration} + onStopGeneration={handleStopGeneration} userId={session?.user?.id} onContextAdd={handleContextAdd} /> @@ -359,7 +367,7 @@ export function Home({ chatId }: HomeProps = {}) { isSending={isSending} isReconnecting={isReconnecting} onSubmit={handleSubmit} - onStopGeneration={stopGeneration} + onStopGeneration={handleStopGeneration} messageQueue={messageQueue} onRemoveQueuedMessage={removeFromQueue} onSendQueuedMessage={sendNow} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 4d485c763ce..da51910789b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -4,6 +4,7 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { History, Plus, Square } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' +import { usePostHog } from 'posthog-js/react' import { useShallow } from 'zustand/react/shallow' import { BubbleChatClose, @@ -33,6 +34,7 @@ import { import { Lock, Unlock, Upload } from '@/components/emcn/icons' import { VariableIcon } from '@/components/icons' import { useSession } from '@/lib/auth/auth-client' +import { captureEvent } from '@/lib/posthog/client' import { generateWorkflowJson } from '@/lib/workflows/operations/import-export' import { ConversationListItem } from '@/app/workspace/[workspaceId]/components' import { MothershipChat } from '@/app/workspace/[workspaceId]/home/components' @@ -101,6 +103,9 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel const params = useParams() const workspaceId = propWorkspaceId ?? (params.workspaceId as string) + const posthog = usePostHog() + const posthogRef = useRef(posthog) + const panelRef = useRef(null) const fileInputRef = useRef(null) const { activeTab, setActiveTab, panelWidth, _hasHydrated, setHasHydrated } = usePanelStore( @@ -264,6 +269,10 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel loadCopilotChats() }, [loadCopilotChats]) + useEffect(() => { + posthogRef.current = posthog + }, [posthog]) + const handleCopilotSelectChat = useCallback((chat: { id: string; title: string | null }) => { setCopilotChatId(chat.id) setCopilotChatTitle(chat.title) @@ -394,6 +403,14 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel [copilotEditQueuedMessage] ) + const handleCopilotStopGeneration = useCallback(() => { + captureEvent(posthogRef.current, 'task_generation_aborted', { + workspace_id: workspaceId, + view: 'copilot', + }) + copilotStopGeneration() + }, [copilotStopGeneration, workspaceId]) + const handleCopilotSubmit = useCallback( (text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => { const trimmed = text.trim() @@ -833,7 +850,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel isSending={copilotIsSending} isReconnecting={copilotIsReconnecting} onSubmit={handleCopilotSubmit} - onStopGeneration={copilotStopGeneration} + onStopGeneration={handleCopilotStopGeneration} messageQueue={copilotMessageQueue} onRemoveQueuedMessage={copilotRemoveFromQueue} onSendQueuedMessage={copilotSendNow} diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index 537a9864282..faf9895bf62 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -378,6 +378,11 @@ export interface PostHogEventMap { workspace_id: string } + task_generation_aborted: { + workspace_id: string + view: 'mothership' | 'copilot' + } + task_message_sent: { workspace_id: string has_attachments: boolean From 67340525e83d276aebd67fe1353ed0bc00b3b7e1 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Sat, 18 Apr 2026 11:10:39 +0800 Subject: [PATCH 2/3] fix: use context variables for block outputs in function block code When a function block references another block's output via , the executor previously embedded the full value as a JavaScript literal directly in the code string. For large outputs (>50 KB), this caused the code string to exceed the terminal console display limit, making inputs appear truncated or replaced with { __simTruncated: true } in the UI. Instead, block output references in function block code are now stored as named global variables (__blockRef_N) in the isolated VM context. The code string only contains the compact variable name, keeping it small regardless of the referenced value size. Loop/parallel/env/workflow references are still inlined as literals since the API route has no way to resolve them independently. The _runtimeContextVars key is filtered from sanitizeInputsForLog so it does not appear in execution logs or SSE events. Pre-resolved context variables are merged with any variables produced by the API route resolveCodeVariables, with executor values taking precedence. Fixes #4195 --- apps/sim/app/api/function/execute/route.ts | 6 +- apps/sim/executor/execution/block-executor.ts | 15 ++- .../handlers/function/function-handler.ts | 6 + apps/sim/executor/variables/resolver.ts | 124 ++++++++++++++++++ apps/sim/tools/function/execute.ts | 1 + apps/sim/tools/function/types.ts | 9 ++ 6 files changed, 157 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index e172f31d771..bdc00a151a7 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -719,6 +719,7 @@ export async function POST(req: NextRequest) { blockNameMapping = {}, blockOutputSchemas = {}, workflowVariables = {}, + contextVariables: preResolvedContextVariables = {}, workflowId, workspaceId, isCustomTool = false, @@ -755,7 +756,10 @@ export async function POST(req: NextRequest) { lang ) resolvedCode = codeResolution.resolvedCode - contextVariables = codeResolution.contextVariables + // Merge pre-resolved block output variables from the executor. These take precedence + // because they were produced by the resolver using full execution-state context + // (including loop/parallel scope) and should not be overwritten. + contextVariables = { ...codeResolution.contextVariables, ...preResolvedContextVariables } } let jsImports = '' diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 5044eab5639..ce06bae5d73 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -42,7 +42,10 @@ import { } from '@/executor/utils/iteration-context' import { isJSONString } from '@/executor/utils/json' import { filterOutputForLog } from '@/executor/utils/output-filter' -import type { VariableResolver } from '@/executor/variables/resolver' +import { + FUNCTION_BLOCK_CONTEXT_VARS_KEY, + type VariableResolver, +} from '@/executor/variables/resolver' import type { SerializedBlock } from '@/serializer/types' import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants' @@ -108,7 +111,13 @@ export class BlockExecutor { await validateBlockType(ctx.userId, blockType, ctx) } - resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block) + if (block.metadata?.id === BlockType.FUNCTION) { + const { resolvedInputs: fnInputs, contextVariables } = + this.resolver.resolveInputsForFunctionBlock(ctx, node.id, block.config.params, block) + resolvedInputs = { ...fnInputs, [FUNCTION_BLOCK_CONTEXT_VARS_KEY]: contextVariables } + } else { + resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block) + } if (blockLog) { blockLog.input = this.sanitizeInputsForLog(resolvedInputs) @@ -418,7 +427,7 @@ export class BlockExecutor { const result: Record = {} for (const [key, value] of Object.entries(inputs)) { - if (SYSTEM_SUBBLOCK_IDS.includes(key) || key === 'triggerMode') { + if (SYSTEM_SUBBLOCK_IDS.includes(key) || key === 'triggerMode' || key === FUNCTION_BLOCK_CONTEXT_VARS_KEY) { continue } diff --git a/apps/sim/executor/handlers/function/function-handler.ts b/apps/sim/executor/handlers/function/function-handler.ts index 68302412bcb..33558ae4ad6 100644 --- a/apps/sim/executor/handlers/function/function-handler.ts +++ b/apps/sim/executor/handlers/function/function-handler.ts @@ -3,6 +3,7 @@ import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages' import { BlockType } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { collectBlockData } from '@/executor/utils/block-data' +import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' @@ -25,6 +26,10 @@ export class FunctionBlockHandler implements BlockHandler { const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx) + const contextVariables = (inputs[FUNCTION_BLOCK_CONTEXT_VARS_KEY] as + | Record + | undefined) ?? {} + const result = await executeTool( 'function_execute', { @@ -36,6 +41,7 @@ export class FunctionBlockHandler implements BlockHandler { blockData, blockNameMapping, blockOutputSchemas, + contextVariables, _context: { workflowId: ctx.workflowId, workspaceId: ctx.workspaceId, diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts index 88b23d72340..15b24ee117e 100644 --- a/apps/sim/executor/variables/resolver.ts +++ b/apps/sim/executor/variables/resolver.ts @@ -15,6 +15,9 @@ import { import { WorkflowResolver } from '@/executor/variables/resolvers/workflow' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' +/** Key used to carry pre-resolved context variables through the inputs map. */ +export const FUNCTION_BLOCK_CONTEXT_VARS_KEY = '_runtimeContextVars' + const logger = createLogger('VariableResolver') export class VariableResolver { @@ -36,6 +39,63 @@ export class VariableResolver { ] } + /** + * Resolves inputs for function blocks. Block output references in the `code` field + * are stored as named context variables instead of being embedded as JavaScript + * literals, preventing large values from bloating the code string. + * + * Returns the resolved inputs and a `contextVariables` map. Callers should inject + * contextVariables into the function execution request body so the isolated VM can + * access them as global variables. + */ + resolveInputsForFunctionBlock( + ctx: ExecutionContext, + currentNodeId: string, + params: Record, + block: SerializedBlock + ): { resolvedInputs: Record; contextVariables: Record } { + const contextVariables: Record = {} + const resolved: Record = {} + + for (const [key, value] of Object.entries(params)) { + if (key === 'code') { + if (typeof value === 'string') { + resolved[key] = this.resolveCodeWithContextVars( + ctx, + currentNodeId, + value, + undefined, + block, + contextVariables + ) + } else if (Array.isArray(value)) { + resolved[key] = value.map((item: any) => { + if (item && typeof item === 'object' && typeof item.content === 'string') { + return { + ...item, + content: this.resolveCodeWithContextVars( + ctx, + currentNodeId, + item.content, + undefined, + block, + contextVariables + ), + } + } + return item + }) + } else { + resolved[key] = this.resolveValue(ctx, currentNodeId, value, undefined, block) + } + } else { + resolved[key] = this.resolveValue(ctx, currentNodeId, value, undefined, block) + } + } + + return { resolvedInputs: resolved, contextVariables } + } + resolveInputs( ctx: ExecutionContext, currentNodeId: string, @@ -149,6 +209,70 @@ export class VariableResolver { } return value } + /** + * Resolves a code template for a function block. Block output references are stored + * in `contextVarAccumulator` as named variables (e.g. `__blockRef_0`) and replaced + * with those variable names in the returned code string. Non-block references (loop + * items, workflow variables, env vars) are still inlined as literals so they remain + * available without any extra passing mechanism. + */ + private resolveCodeWithContextVars( + ctx: ExecutionContext, + currentNodeId: string, + template: string, + loopScope: LoopScope | undefined, + block: SerializedBlock, + contextVarAccumulator: Record + ): string { + const resolutionContext: ResolutionContext = { + executionContext: ctx, + executionState: this.state, + currentNodeId, + loopScope, + } + + const language = (block.config?.params as Record | undefined)?.language as + | string + | undefined + + let replacementError: Error | null = null + + let result = replaceValidReferences(template, (match) => { + if (replacementError) return match + + try { + const resolved = this.resolveReference(match, resolutionContext) + if (resolved === undefined) return match + + const effectiveValue = resolved === RESOLVED_EMPTY ? null : resolved + + if (this.blockResolver.canResolve(match)) { + // Block output: store in contextVarAccumulator, replace with variable name + const varName = `__blockRef_${Object.keys(contextVarAccumulator).length}` + contextVarAccumulator[varName] = effectiveValue + return varName + } + + // Non-block reference (loop, parallel, workflow, env): embed as literal + return this.blockResolver.formatValueForBlock(effectiveValue, BlockType.FUNCTION, language) + } catch (error) { + replacementError = error instanceof Error ? error : new Error(String(error)) + return match + } + }) + + if (replacementError !== null) { + throw replacementError + } + + result = result.replace(createEnvVarPattern(), (match) => { + const resolved = this.resolveReference(match, resolutionContext) + return typeof resolved === 'string' ? resolved : match + }) + + return result + } + private resolveTemplate( ctx: ExecutionContext, currentNodeId: string, diff --git a/apps/sim/tools/function/execute.ts b/apps/sim/tools/function/execute.ts index 844b1c6d515..59873843cbb 100644 --- a/apps/sim/tools/function/execute.ts +++ b/apps/sim/tools/function/execute.ts @@ -128,6 +128,7 @@ export const functionExecuteTool: ToolConfig blockNameMapping?: Record blockOutputSchemas?: Record> + /** Pre-resolved block output variables from the executor, injected as VM globals. */ + contextVariables?: Record _context?: { workflowId?: string userId?: string @@ -32,3 +34,10 @@ export interface CodeExecutionOutput extends ToolResponse { stdout: string } } + +export interface CodeExecutionOutput extends ToolResponse { + output: { + result: any + stdout: string + } +} From 27836065ae81168c61174ed4f1c4acadb2983005 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Sat, 18 Apr 2026 13:22:30 +0800 Subject: [PATCH 3/3] fix: address Cursor and Greptile bot review comments - Pass preResolvedContextVariables through to shellEnvs for Shell language (Cursor: shell loses pre-resolved block refs, executes against undefined vars) - Remove duplicate CodeExecutionOutput interface declaration (Cursor + Greptile: dead duplicate declaration in tools/function/types.ts) - Deduplicate identical block references in resolveCodeWithContextVars so the same reused multiple times shares one __blockRef_N slot (Greptile P2: avoid duplicating large payloads across the wire) --- apps/sim/app/api/function/execute/route.ts | 4 ++++ apps/sim/executor/variables/resolver.ts | 21 +++++++++++++++++---- apps/sim/tools/function/types.ts | 7 ------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index bdc00a151a7..bc0070ed58a 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -744,6 +744,10 @@ export async function POST(req: NextRequest) { // For shell, env vars are injected as OS env vars via shellEnvs. // Replace {{VAR}} placeholders with $VAR so the shell can access them natively. resolvedCode = code.replace(/\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}/g, '$$$1') + // Carry pre-resolved block output variables (e.g. __blockRef_N) so they can be + // injected as shell env vars below. The executor replaces block references in the + // code with these names, so the values must be present at runtime. + contextVariables = { ...preResolvedContextVariables } } else { const codeResolution = resolveCodeVariables( code, diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts index 15b24ee117e..1294675c2b3 100644 --- a/apps/sim/executor/variables/resolver.ts +++ b/apps/sim/executor/variables/resolver.ts @@ -237,22 +237,35 @@ export class VariableResolver { let replacementError: Error | null = null + const blockRefByMatch = new Map() + let result = replaceValidReferences(template, (match) => { if (replacementError) return match try { - const resolved = this.resolveReference(match, resolutionContext) - if (resolved === undefined) return match + if (this.blockResolver.canResolve(match)) { + // Deduplicate: identical references in the same template share a single + // accumulator slot so we do not duplicate large payloads. + const existing = blockRefByMatch.get(match) + if (existing !== undefined) return existing - const effectiveValue = resolved === RESOLVED_EMPTY ? null : resolved + const resolved = this.resolveReference(match, resolutionContext) + if (resolved === undefined) return match + + const effectiveValue = resolved === RESOLVED_EMPTY ? null : resolved - if (this.blockResolver.canResolve(match)) { // Block output: store in contextVarAccumulator, replace with variable name const varName = `__blockRef_${Object.keys(contextVarAccumulator).length}` contextVarAccumulator[varName] = effectiveValue + blockRefByMatch.set(match, varName) return varName } + const resolved = this.resolveReference(match, resolutionContext) + if (resolved === undefined) return match + + const effectiveValue = resolved === RESOLVED_EMPTY ? null : resolved + // Non-block reference (loop, parallel, workflow, env): embed as literal return this.blockResolver.formatValueForBlock(effectiveValue, BlockType.FUNCTION, language) } catch (error) { diff --git a/apps/sim/tools/function/types.ts b/apps/sim/tools/function/types.ts index f1945fb1b99..b46aee1561b 100644 --- a/apps/sim/tools/function/types.ts +++ b/apps/sim/tools/function/types.ts @@ -34,10 +34,3 @@ export interface CodeExecutionOutput extends ToolResponse { stdout: string } } - -export interface CodeExecutionOutput extends ToolResponse { - output: { - result: any - stdout: string - } -}