fix(mothership): fix edit hashing (#3711)

This commit is contained in:
Siddharth Ganesan
2026-03-22 03:06:58 -07:00
committed by GitHub
parent 4cb5e3469f
commit 9d6a7f3970
4 changed files with 31 additions and 96 deletions

View File

@@ -1,11 +1,9 @@
import { createLogger } from '@sim/logger'
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types'
import { getOrMaterializeVFS } from '@/lib/copilot/vfs'
import { upsertWorkflowReadHashForSanitizedState } from '@/lib/copilot/workflow-read-hashes'
import { listChatUploads, readChatUpload } from './upload-file-reader'
const logger = createLogger('VfsTools')
const WORKFLOW_STATE_PATH_REGEX = /^workflows\/[^/]+\/state\.json$/
export async function executeVfsGrep(
params: Record<string, unknown>,
@@ -145,28 +143,6 @@ export async function executeVfsRead(
return { success: false, error: `File not found: ${path}.${hint}` }
}
logger.debug('vfs_read result', { path, totalLines: result.totalLines })
if (context.chatId && WORKFLOW_STATE_PATH_REGEX.test(path)) {
try {
const fullState = vfs.read(path)
const fullMeta = vfs.read(path.replace(/state\.json$/, 'meta.json'))
if (fullState?.content && fullMeta?.content) {
const workflowMeta = JSON.parse(fullMeta.content) as { id?: string }
const sanitizedState = JSON.parse(fullState.content)
if (workflowMeta.id) {
await upsertWorkflowReadHashForSanitizedState(
context.chatId,
workflowMeta.id,
sanitizedState
)
}
}
} catch (hashErr) {
logger.warn('Failed to persist workflow read hash from VFS read', {
path,
error: hashErr instanceof Error ? hashErr.message : String(hashErr),
})
}
}
return {
success: true,
output: result,

View File

@@ -2,7 +2,6 @@ import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { createWorkspaceApiKey } from '@/lib/api-key/auth'
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types'
import { upsertWorkflowReadHashForWorkflowState } from '@/lib/copilot/workflow-read-hashes'
import { generateRequestId } from '@/lib/core/utils/request'
import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow'
import {
@@ -10,6 +9,7 @@ import {
getLatestExecutionState,
} from '@/lib/workflows/executor/execution-state'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
import {
createFolderRecord,
createWorkflowRecord,
@@ -132,36 +132,26 @@ export async function executeCreateWorkflow(
folderId,
})
const normalized = await loadWorkflowFromNormalizedTables(result.workflowId)
let copilotSanitizedWorkflowState: unknown
if (normalized) {
copilotSanitizedWorkflowState = sanitizeForCopilot({
blocks: normalized.blocks || {},
edges: normalized.edges || [],
loops: normalized.loops || {},
parallels: normalized.parallels || {},
} as any)
}
return {
success: true,
output: await (async () => {
let workflowReadHash: string | undefined
if (context.chatId) {
assertWorkflowMutationNotAborted(context)
const normalized = await loadWorkflowFromNormalizedTables(result.workflowId)
if (normalized) {
const seeded = await upsertWorkflowReadHashForWorkflowState(
context.chatId,
result.workflowId,
{
blocks: normalized.blocks || {},
edges: normalized.edges || [],
loops: normalized.loops || {},
parallels: normalized.parallels || {},
}
)
workflowReadHash = seeded.hash
}
}
return {
workflowId: result.workflowId,
workflowName: result.name,
workspaceId: result.workspaceId,
folderId: result.folderId,
...(workflowReadHash ? { workflowReadHash } : {}),
}
})(),
output: {
workflowId: result.workflowId,
workflowName: result.name,
workspaceId: result.workspaceId,
folderId: result.folderId,
...(copilotSanitizedWorkflowState ? { copilotSanitizedWorkflowState } : {}),
},
}
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) }

View File

@@ -7,7 +7,6 @@ import {
serializeTableMeta,
serializeWorkflowMeta,
} from '@/lib/copilot/vfs/serializers'
import { upsertWorkflowReadHashForSanitizedState } from '@/lib/copilot/workflow-read-hashes'
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags'
import { getTableById } from '@/lib/table/service'
import { canAccessTemplate } from '@/lib/templates/permissions'
@@ -371,9 +370,6 @@ async function processWorkflowFromDb(
parallels: normalized.parallels || {},
}
const sanitizedState = sanitizeForCopilot(workflowState)
if (chatId) {
await upsertWorkflowReadHashForSanitizedState(chatId, workflowId, sanitizedState)
}
const content = JSON.stringify(
{
workflowId,
@@ -757,7 +753,7 @@ export async function resolveActiveResourceContext(
resourceId,
undefined,
'@active_resource',
'workflow',
'current_workflow',
undefined,
chatId
)

View File

@@ -7,11 +7,6 @@ import {
type BaseServerTool,
type ServerToolContext,
} from '@/lib/copilot/tools/server/base-tool'
import {
computeWorkflowReadHashFromWorkflowState,
getWorkflowReadHash,
upsertWorkflowReadHashForWorkflowState,
} from '@/lib/copilot/workflow-read-hashes'
import { applyTargetedLayout } from '@/lib/workflows/autolayout'
import {
DEFAULT_HORIZONTAL_SPACING,
@@ -98,33 +93,19 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
chatId: context.chatId,
})
if (!context.chatId) {
throw new Error(
'Workflow has changed or was not read in this chat. Re-read the workflow before editing.'
)
}
assertServerToolNotAborted(context)
const fromDb = await getCurrentWorkflowStateFromDb(workflowId)
const workflowState = fromDb.workflowState
const storedReadHash = await getWorkflowReadHash(context.chatId, workflowId)
if (!storedReadHash) {
throw new Error(
'Workflow has changed or was not read in this chat. Re-read the workflow before editing.'
)
}
const currentReadState = computeWorkflowReadHashFromWorkflowState({
blocks: workflowState.blocks || {},
edges: workflowState.edges || [],
loops: workflowState.loops || {},
parallels: workflowState.parallels || {},
})
if (storedReadHash !== currentReadState.hash) {
throw new Error(
'Workflow changed since it was last read in this chat. Re-read the workflow before editing.'
)
let workflowState: any
if (currentUserWorkflow) {
try {
workflowState = JSON.parse(currentUserWorkflow)
} catch (error) {
logger.error('Failed to parse currentUserWorkflow', error)
throw new Error('Invalid currentUserWorkflow format')
}
} else {
const fromDb = await getCurrentWorkflowStateFromDb(workflowId)
workflowState = fromDb.workflowState
}
// Get permission config for the user
@@ -318,20 +299,12 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
logger.info('Workflow state persisted to database', { workflowId })
const sanitizationWarnings = validation.warnings.length > 0 ? validation.warnings : undefined
assertServerToolNotAborted(context)
const updatedReadState = await upsertWorkflowReadHashForWorkflowState(
context.chatId,
workflowId,
workflowStateForDb
)
return {
success: true,
workflowId,
workflowName: workflowName ?? 'Workflow',
workflowState: { ...finalWorkflowState, blocks: layoutedBlocks },
copilotSanitizedWorkflowState: updatedReadState.sanitizedState,
workflowReadHash: updatedReadState.hash,
...(inputErrors && {
inputValidationErrors: inputErrors,
inputValidationMessage: `${inputErrors.length} input(s) were rejected due to validation errors. The workflow was still updated with valid inputs only. Errors: ${inputErrors.join('; ')}`,