Skip to content

Commit 9d6a7f3

Browse files
authored
fix(mothership): fix edit hashing (#3711)
1 parent 4cb5e34 commit 9d6a7f3

File tree

4 files changed

+31
-96
lines changed

4 files changed

+31
-96
lines changed

apps/sim/lib/copilot/orchestrator/tool-executor/vfs-tools.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { createLogger } from '@sim/logger'
22
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types'
33
import { getOrMaterializeVFS } from '@/lib/copilot/vfs'
4-
import { upsertWorkflowReadHashForSanitizedState } from '@/lib/copilot/workflow-read-hashes'
54
import { listChatUploads, readChatUpload } from './upload-file-reader'
65

76
const logger = createLogger('VfsTools')
8-
const WORKFLOW_STATE_PATH_REGEX = /^workflows\/[^/]+\/state\.json$/
97

108
export async function executeVfsGrep(
119
params: Record<string, unknown>,
@@ -145,28 +143,6 @@ export async function executeVfsRead(
145143
return { success: false, error: `File not found: ${path}.${hint}` }
146144
}
147145
logger.debug('vfs_read result', { path, totalLines: result.totalLines })
148-
if (context.chatId && WORKFLOW_STATE_PATH_REGEX.test(path)) {
149-
try {
150-
const fullState = vfs.read(path)
151-
const fullMeta = vfs.read(path.replace(/state\.json$/, 'meta.json'))
152-
if (fullState?.content && fullMeta?.content) {
153-
const workflowMeta = JSON.parse(fullMeta.content) as { id?: string }
154-
const sanitizedState = JSON.parse(fullState.content)
155-
if (workflowMeta.id) {
156-
await upsertWorkflowReadHashForSanitizedState(
157-
context.chatId,
158-
workflowMeta.id,
159-
sanitizedState
160-
)
161-
}
162-
}
163-
} catch (hashErr) {
164-
logger.warn('Failed to persist workflow read hash from VFS read', {
165-
path,
166-
error: hashErr instanceof Error ? hashErr.message : String(hashErr),
167-
})
168-
}
169-
}
170146
return {
171147
success: true,
172148
output: result,

apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import crypto from 'crypto'
22
import { createLogger } from '@sim/logger'
33
import { createWorkspaceApiKey } from '@/lib/api-key/auth'
44
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types'
5-
import { upsertWorkflowReadHashForWorkflowState } from '@/lib/copilot/workflow-read-hashes'
65
import { generateRequestId } from '@/lib/core/utils/request'
76
import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow'
87
import {
98
getExecutionState,
109
getLatestExecutionState,
1110
} from '@/lib/workflows/executor/execution-state'
1211
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
12+
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
1313
import {
1414
createFolderRecord,
1515
createWorkflowRecord,
@@ -132,36 +132,26 @@ export async function executeCreateWorkflow(
132132
folderId,
133133
})
134134

135+
const normalized = await loadWorkflowFromNormalizedTables(result.workflowId)
136+
let copilotSanitizedWorkflowState: unknown
137+
if (normalized) {
138+
copilotSanitizedWorkflowState = sanitizeForCopilot({
139+
blocks: normalized.blocks || {},
140+
edges: normalized.edges || [],
141+
loops: normalized.loops || {},
142+
parallels: normalized.parallels || {},
143+
} as any)
144+
}
145+
135146
return {
136147
success: true,
137-
output: await (async () => {
138-
let workflowReadHash: string | undefined
139-
if (context.chatId) {
140-
assertWorkflowMutationNotAborted(context)
141-
const normalized = await loadWorkflowFromNormalizedTables(result.workflowId)
142-
if (normalized) {
143-
const seeded = await upsertWorkflowReadHashForWorkflowState(
144-
context.chatId,
145-
result.workflowId,
146-
{
147-
blocks: normalized.blocks || {},
148-
edges: normalized.edges || [],
149-
loops: normalized.loops || {},
150-
parallels: normalized.parallels || {},
151-
}
152-
)
153-
workflowReadHash = seeded.hash
154-
}
155-
}
156-
157-
return {
158-
workflowId: result.workflowId,
159-
workflowName: result.name,
160-
workspaceId: result.workspaceId,
161-
folderId: result.folderId,
162-
...(workflowReadHash ? { workflowReadHash } : {}),
163-
}
164-
})(),
148+
output: {
149+
workflowId: result.workflowId,
150+
workflowName: result.name,
151+
workspaceId: result.workspaceId,
152+
folderId: result.folderId,
153+
...(copilotSanitizedWorkflowState ? { copilotSanitizedWorkflowState } : {}),
154+
},
165155
}
166156
} catch (error) {
167157
return { success: false, error: error instanceof Error ? error.message : String(error) }

apps/sim/lib/copilot/process-contents.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
serializeTableMeta,
88
serializeWorkflowMeta,
99
} from '@/lib/copilot/vfs/serializers'
10-
import { upsertWorkflowReadHashForSanitizedState } from '@/lib/copilot/workflow-read-hashes'
1110
import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags'
1211
import { getTableById } from '@/lib/table/service'
1312
import { canAccessTemplate } from '@/lib/templates/permissions'
@@ -371,9 +370,6 @@ async function processWorkflowFromDb(
371370
parallels: normalized.parallels || {},
372371
}
373372
const sanitizedState = sanitizeForCopilot(workflowState)
374-
if (chatId) {
375-
await upsertWorkflowReadHashForSanitizedState(chatId, workflowId, sanitizedState)
376-
}
377373
const content = JSON.stringify(
378374
{
379375
workflowId,
@@ -757,7 +753,7 @@ export async function resolveActiveResourceContext(
757753
resourceId,
758754
undefined,
759755
'@active_resource',
760-
'workflow',
756+
'current_workflow',
761757
undefined,
762758
chatId
763759
)

apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@ import {
77
type BaseServerTool,
88
type ServerToolContext,
99
} from '@/lib/copilot/tools/server/base-tool'
10-
import {
11-
computeWorkflowReadHashFromWorkflowState,
12-
getWorkflowReadHash,
13-
upsertWorkflowReadHashForWorkflowState,
14-
} from '@/lib/copilot/workflow-read-hashes'
1510
import { applyTargetedLayout } from '@/lib/workflows/autolayout'
1611
import {
1712
DEFAULT_HORIZONTAL_SPACING,
@@ -98,33 +93,19 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
9893
chatId: context.chatId,
9994
})
10095

101-
if (!context.chatId) {
102-
throw new Error(
103-
'Workflow has changed or was not read in this chat. Re-read the workflow before editing.'
104-
)
105-
}
106-
10796
assertServerToolNotAborted(context)
10897

109-
const fromDb = await getCurrentWorkflowStateFromDb(workflowId)
110-
const workflowState = fromDb.workflowState
111-
const storedReadHash = await getWorkflowReadHash(context.chatId, workflowId)
112-
if (!storedReadHash) {
113-
throw new Error(
114-
'Workflow has changed or was not read in this chat. Re-read the workflow before editing.'
115-
)
116-
}
117-
118-
const currentReadState = computeWorkflowReadHashFromWorkflowState({
119-
blocks: workflowState.blocks || {},
120-
edges: workflowState.edges || [],
121-
loops: workflowState.loops || {},
122-
parallels: workflowState.parallels || {},
123-
})
124-
if (storedReadHash !== currentReadState.hash) {
125-
throw new Error(
126-
'Workflow changed since it was last read in this chat. Re-read the workflow before editing.'
127-
)
98+
let workflowState: any
99+
if (currentUserWorkflow) {
100+
try {
101+
workflowState = JSON.parse(currentUserWorkflow)
102+
} catch (error) {
103+
logger.error('Failed to parse currentUserWorkflow', error)
104+
throw new Error('Invalid currentUserWorkflow format')
105+
}
106+
} else {
107+
const fromDb = await getCurrentWorkflowStateFromDb(workflowId)
108+
workflowState = fromDb.workflowState
128109
}
129110

130111
// Get permission config for the user
@@ -318,20 +299,12 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
318299
logger.info('Workflow state persisted to database', { workflowId })
319300

320301
const sanitizationWarnings = validation.warnings.length > 0 ? validation.warnings : undefined
321-
assertServerToolNotAborted(context)
322-
const updatedReadState = await upsertWorkflowReadHashForWorkflowState(
323-
context.chatId,
324-
workflowId,
325-
workflowStateForDb
326-
)
327302

328303
return {
329304
success: true,
330305
workflowId,
331306
workflowName: workflowName ?? 'Workflow',
332307
workflowState: { ...finalWorkflowState, blocks: layoutedBlocks },
333-
copilotSanitizedWorkflowState: updatedReadState.sanitizedState,
334-
workflowReadHash: updatedReadState.hash,
335308
...(inputErrors && {
336309
inputValidationErrors: inputErrors,
337310
inputValidationMessage: `${inputErrors.length} input(s) were rejected due to validation errors. The workflow was still updated with valid inputs only. Errors: ${inputErrors.join('; ')}`,

0 commit comments

Comments
 (0)