Skip to content

Commit 526673a

Browse files
committed
fix(fork): call Go before Sim INSERT, update route baseline
1 parent 04968a3 commit 526673a

2 files changed

Lines changed: 39 additions & 42 deletions

File tree

apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -82,37 +82,15 @@ export const POST = withRouteHandler(
8282
const title = `Fork | ${baseTitle}`
8383
const now = new Date()
8484

85-
const [newChat] = await db
86-
.insert(copilotChats)
87-
.values({
88-
id: newId,
89-
userId,
90-
workspaceId: parent.workspaceId,
91-
workflowId: parent.workflowId,
92-
type: parent.type,
93-
title,
94-
model: parent.model,
95-
messages: forkedMessages,
96-
resources: parentResources,
97-
previewYaml: parent.previewYaml,
98-
planArtifact: parent.planArtifact,
99-
config: parent.config,
100-
conversationId: null,
101-
updatedAt: now,
102-
lastSeenAt: now,
103-
})
104-
.returning({ id: copilotChats.id, workspaceId: copilotChats.workspaceId })
105-
106-
if (!newChat) {
107-
return createInternalServerErrorResponse('Failed to create forked chat')
108-
}
109-
110-
// Clone copilot-service conversation state (messages, active_messages, memory files).
85+
// Clone copilot-service conversation state first. If this fails we never
86+
// insert the Sim row, so there is no orphaned UI entry to clean up.
87+
// (The inverse order — Sim INSERT first — required a compensating delete
88+
// and still left a brief window where the row was visible but Go state
89+
// wasn't ready.)
11190
const copilotHeaders: Record<string, string> = { 'Content-Type': 'application/json' }
11291
if (env.COPILOT_API_KEY) {
11392
copilotHeaders['x-api-key'] = env.COPILOT_API_KEY
11493
}
115-
let copilotFailed = false
11694
try {
11795
const copilotRes = await fetchGo(`${SIM_AGENT_API_URL}/api/chats/fork`, {
11896
method: 'POST',
@@ -129,24 +107,43 @@ export const POST = withRouteHandler(
129107
if (!copilotRes.ok) {
130108
const text = await copilotRes.text().catch(() => '')
131109
logger.error('Copilot fork returned non-OK', { status: copilotRes.status, body: text })
132-
copilotFailed = true
110+
return createInternalServerErrorResponse('Failed to fork chat')
133111
}
134112
} catch (err) {
135113
logger.error('Failed to call copilot fork endpoint', { err })
136-
copilotFailed = true
114+
return createInternalServerErrorResponse('Failed to fork chat')
137115
}
138116

139-
if (copilotFailed) {
140-
// Compensating delete — remove the orphaned Sim row.
141-
await db
142-
.delete(copilotChats)
143-
.where(eq(copilotChats.id, newId))
144-
.catch((e: unknown) => {
145-
logger.error('Failed to delete orphaned forked chat after copilot failure', {
146-
error: e,
147-
})
148-
})
149-
return createInternalServerErrorResponse('Failed to fork chat')
117+
// Go state is ready — now persist the Sim metadata row. If this insert
118+
// fails the Go conversation is orphaned but permanently inaccessible
119+
// (no Sim row = no UI entry), which is harmless.
120+
const [newChat] = await db
121+
.insert(copilotChats)
122+
.values({
123+
id: newId,
124+
userId,
125+
workspaceId: parent.workspaceId,
126+
workflowId: parent.workflowId,
127+
type: parent.type,
128+
title,
129+
model: parent.model,
130+
messages: forkedMessages,
131+
resources: parentResources,
132+
previewYaml: parent.previewYaml,
133+
planArtifact: parent.planArtifact,
134+
config: parent.config,
135+
conversationId: null,
136+
updatedAt: now,
137+
lastSeenAt: now,
138+
})
139+
.returning({ id: copilotChats.id, workspaceId: copilotChats.workspaceId })
140+
141+
if (!newChat) {
142+
logger.error('Failed to insert forked chat row after successful Go fork', {
143+
newId,
144+
chatId,
145+
})
146+
return createInternalServerErrorResponse('Failed to create forked chat')
150147
}
151148

152149
if (newChat.workspaceId) {

scripts/check-api-validation-contracts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries')
99
const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors')
1010

1111
const BASELINE = {
12-
totalRoutes: 716,
13-
zodRoutes: 716,
12+
totalRoutes: 717,
13+
zodRoutes: 717,
1414
nonZodRoutes: 0,
1515
} as const
1616

0 commit comments

Comments
 (0)