chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,180 @@
import { db } from '@sim/db'
import { copilotChats } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { forkMothershipChatContract } from '@/lib/api/contracts/mothership-chats'
import { parseRequest } from '@/lib/api/server'
import { loadCopilotChatMessages } from '@/lib/copilot/chat/lifecycle'
import { appendCopilotChatMessages } from '@/lib/copilot/chat/messages-store'
import { chatPubSub } from '@/lib/copilot/chat-status'
import { fetchGo } from '@/lib/copilot/request/go/fetch'
import {
authenticateCopilotRequestSessionOnly,
createBadRequestResponse,
createForbiddenResponse,
createInternalServerErrorResponse,
createNotFoundResponse,
createUnauthorizedResponse,
} from '@/lib/copilot/request/http'
import type { MothershipResource } from '@/lib/copilot/resources/types'
import { getMothershipBaseURL, getMothershipSourceEnvHeaders } from '@/lib/copilot/server/agent-url'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { captureServerEvent } from '@/lib/posthog/server'
import {
assertActiveWorkspaceAccess,
isWorkspaceAccessDeniedError,
} from '@/lib/workspaces/permissions/utils'
const logger = createLogger('ForkChatAPI')
/**
* POST /api/mothership/chats/[chatId]/fork
* Creates a new chat branched from the given chat, keeping messages up to and
* including the specified message. Resources and copilot-side state are copied.
*/
export const POST = withRouteHandler(
async (request: NextRequest, context: { params: Promise<{ chatId: string }> }) => {
try {
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
if (!isAuthenticated || !userId) {
return createUnauthorizedResponse()
}
const parsed = await parseRequest(forkMothershipChatContract, request, context, {
validationErrorResponse: () => createBadRequestResponse('upToMessageId is required'),
})
if (!parsed.success) return parsed.response
const { chatId } = parsed.data.params
const { upToMessageId } = parsed.data.body
const [parent] = await db
.select({
id: copilotChats.id,
userId: copilotChats.userId,
type: copilotChats.type,
workspaceId: copilotChats.workspaceId,
title: copilotChats.title,
model: copilotChats.model,
resources: copilotChats.resources,
previewYaml: copilotChats.previewYaml,
planArtifact: copilotChats.planArtifact,
config: copilotChats.config,
})
.from(copilotChats)
.where(eq(copilotChats.id, chatId))
.limit(1)
if (!parent || parent.userId !== userId || parent.type !== 'mothership') {
return createNotFoundResponse('Chat not found')
}
if (parent.workspaceId) {
await assertActiveWorkspaceAccess(parent.workspaceId, userId)
}
const messages = await loadCopilotChatMessages(chatId)
const forkIdx = messages.findIndex((m) => m.id === upToMessageId)
if (forkIdx < 0) {
return createBadRequestResponse('Message not found in chat')
}
const forkedMessages = messages.slice(0, forkIdx + 1)
// Resources are stored as a jsonb array on the chat row — copy them directly.
const parentResources = Array.isArray(parent.resources)
? (parent.resources as MothershipResource[])
: []
const newId = generateId()
const baseTitle = (parent.title ?? 'New chat').replace(/^Fork \| /, '')
const title = `Fork | ${baseTitle}`
const now = new Date()
const newChat = await db.transaction(async (tx) => {
const [row] = await tx
.insert(copilotChats)
.values({
id: newId,
userId,
workspaceId: parent.workspaceId,
type: parent.type,
title,
model: parent.model,
resources: parentResources,
previewYaml: parent.previewYaml,
planArtifact: parent.planArtifact,
config: parent.config,
conversationId: null,
updatedAt: now,
lastSeenAt: now,
})
.returning({ id: copilotChats.id, workspaceId: copilotChats.workspaceId })
if (!row) return null
await appendCopilotChatMessages(newId, forkedMessages, { chatModel: parent.model }, tx)
return row
})
if (!newChat) {
return createInternalServerErrorResponse('Failed to create forked chat')
}
// Clone copilot-service conversation state (messages, active_messages, memory files).
// Best-effort: if the copilot service doesn't have a row for the source chat yet, skip.
try {
const copilotHeaders: Record<string, string> = { 'Content-Type': 'application/json' }
if (env.COPILOT_API_KEY) {
copilotHeaders['x-api-key'] = env.COPILOT_API_KEY
}
Object.assign(copilotHeaders, getMothershipSourceEnvHeaders())
const mothershipBaseURL = await getMothershipBaseURL({ userId })
const copilotRes = await fetchGo(`${mothershipBaseURL}/api/chats/fork`, {
method: 'POST',
headers: copilotHeaders,
body: JSON.stringify({
sourceChatId: chatId,
newChatId: newId,
upToMessageId,
userId,
}),
spanName: 'sim → go /api/chats/fork',
operation: 'fork_chat',
})
if (!copilotRes.ok) {
const text = await copilotRes.text().catch(() => '')
logger.warn('Copilot fork returned non-OK', { status: copilotRes.status, body: text })
}
} catch (err) {
// The copilot service may not have a row for this chat if no messages
// have been sent yet, or if it's unreachable. Log and continue.
logger.warn('Failed to fork copilot-service conversation, skipping', { err })
}
if (newChat.workspaceId) {
chatPubSub?.publishStatusChanged({
workspaceId: newChat.workspaceId,
chatId: newId,
type: 'created',
})
}
captureServerEvent(
userId,
'task_forked',
{ workspace_id: parent.workspaceId ?? '', source_chat_id: chatId },
{ groups: { workspace: parent.workspaceId ?? '' } }
)
return NextResponse.json({ success: true, id: newId })
} catch (error) {
if (isWorkspaceAccessDeniedError(error)) {
return createForbiddenResponse('Workspace access denied')
}
logger.error('Error forking chat:', error)
return createInternalServerErrorResponse('Failed to fork chat')
}
}
)