Skip to content

Commit 04968a3

Browse files
committed
fix(fork): resolve P0/P1 bugs — protocol, atomicity, streaming guard, button
- Send keepCount instead of upToMessageId to Go; the Sim persisted-message array index is used directly so there is no ID-matching between Sim and Go. - Add streaming guard: reject fork if parent.conversationId is set (active stream in progress), matching the /chat/stop pattern. - Add workflowId to INSERT so forked chats inherit the parent workflow link. - Make Go failure a hard error with compensating delete: if the copilot call returns non-OK or throws, the orphaned Sim row is deleted before returning 500 instead of silently succeeding. - Move taskPubSub and captureServerEvent into the success path so analytics and sidebar updates only fire when both services confirmed the fork. - Enable the fork button: canFork = Boolean(messageId && !isStreamActive). - Add isStreamActive prop to MessageActions; mothership-chat passes it down so the button is suppressed during active generations.
1 parent a9c12a2 commit 04968a3

3 files changed

Lines changed: 32 additions & 11 deletions

File tree

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

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export const POST = withRouteHandler(
6060
await assertActiveWorkspaceAccess(parent.workspaceId, userId)
6161
}
6262

63+
if (parent.conversationId) {
64+
return createBadRequestResponse('Cannot fork a chat with an active stream')
65+
}
66+
6367
// Find the fork point in the Sim-side messages array.
6468
const messages = Array.isArray(parent.messages) ? (parent.messages as PersistedMessage[]) : []
6569
const forkIdx = messages.findIndex((m) => m.id === upToMessageId)
@@ -84,6 +88,7 @@ export const POST = withRouteHandler(
8488
id: newId,
8589
userId,
8690
workspaceId: parent.workspaceId,
91+
workflowId: parent.workflowId,
8792
type: parent.type,
8893
title,
8994
model: parent.model,
@@ -103,32 +108,45 @@ export const POST = withRouteHandler(
103108
}
104109

105110
// Clone copilot-service conversation state (messages, active_messages, memory files).
106-
// Best-effort: if the copilot service doesn't have a row for the source chat yet, skip.
111+
const copilotHeaders: Record<string, string> = { 'Content-Type': 'application/json' }
112+
if (env.COPILOT_API_KEY) {
113+
copilotHeaders['x-api-key'] = env.COPILOT_API_KEY
114+
}
115+
let copilotFailed = false
107116
try {
108-
const copilotHeaders: Record<string, string> = { 'Content-Type': 'application/json' }
109-
if (env.COPILOT_API_KEY) {
110-
copilotHeaders['x-api-key'] = env.COPILOT_API_KEY
111-
}
112117
const copilotRes = await fetchGo(`${SIM_AGENT_API_URL}/api/chats/fork`, {
113118
method: 'POST',
114119
headers: copilotHeaders,
115120
body: JSON.stringify({
116121
sourceChatId: chatId,
117122
newChatId: newId,
118-
upToMessageId,
123+
keepCount: forkedMessages.length,
119124
userId,
120125
}),
121126
spanName: 'sim → go /api/chats/fork',
122127
operation: 'fork_chat',
123128
})
124129
if (!copilotRes.ok) {
125130
const text = await copilotRes.text().catch(() => '')
126-
logger.warn('Copilot fork returned non-OK', { status: copilotRes.status, body: text })
131+
logger.error('Copilot fork returned non-OK', { status: copilotRes.status, body: text })
132+
copilotFailed = true
127133
}
128134
} catch (err) {
129-
// The copilot service may not have a row for this chat if no messages
130-
// have been sent yet, or if it's unreachable. Log and continue.
131-
logger.warn('Failed to fork copilot-service conversation, skipping', { err })
135+
logger.error('Failed to call copilot fork endpoint', { err })
136+
copilotFailed = true
137+
}
138+
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')
132150
}
133151

134152
if (newChat.workspaceId) {

apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ interface MessageActionsProps {
5555
userQuery?: string
5656
requestId?: string
5757
messageId?: string
58+
isStreamActive?: boolean
5859
}
5960

6061
export const MessageActions = memo(function MessageActions({
@@ -63,6 +64,7 @@ export const MessageActions = memo(function MessageActions({
6364
userQuery,
6465
requestId,
6566
messageId,
67+
isStreamActive,
6668
}: MessageActionsProps) {
6769
const router = useRouter()
6870
const params = useParams<{ workspaceId: string }>()
@@ -164,7 +166,7 @@ export const MessageActions = memo(function MessageActions({
164166

165167
const hasContent = Boolean(content)
166168
const canSubmitFeedback = Boolean(chatId && userQuery)
167-
const canFork = false
169+
const canFork = Boolean(messageId && !isStreamActive)
168170
if (!hasContent && !canSubmitFeedback && !canFork) return null
169171

170172
return (

apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export function MothershipChat({
220220
userQuery={precedingUserContent}
221221
requestId={msg.requestId}
222222
messageId={msg.id}
223+
isStreamActive={isStreamActive}
223224
/>
224225
</div>
225226
)}

0 commit comments

Comments
 (0)