Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5028875
feat(transport): replace shared chat transport with mothership-stream…
Sg312 Mar 25, 2026
448ea02
improvement(contracts): regenerate contracts from go
Sg312 Mar 25, 2026
7c5547f
feat(tools): add tool catalog codegen from go tool contracts
Sg312 Mar 25, 2026
d5131ae
feat(tools): add tool-executor dispatch framework for sim side tool r…
Sg312 Mar 26, 2026
1157dfc
feat(orchestrator): rewrite tool dispatch with catalog-driven executo…
Sg312 Mar 26, 2026
997896d
feat(orchestrator): checkpoint resume flow
Sg312 Mar 26, 2026
f9c185b
refactor(copilot): consolidate orchestrator into request/ layer
Sg312 Mar 27, 2026
97d41e9
refactor(mothership): reorganize lib/copilot into structured subdirec…
Sg312 Mar 27, 2026
c4876ba
refactor(mothership): canonical transcript layer, dead code cleanup, …
Sg312 Mar 27, 2026
410dd9a
refactor(mothership): rebase onto latest staging
Sg312 Mar 27, 2026
741d856
refactor(mothership): rename request continue to lifecycle
Sg312 Mar 27, 2026
407d254
feat(trace): add initial version of request traces
Sg312 Mar 28, 2026
4b3b6ae
improvement(stream): batch stream from redis
Sg312 Mar 28, 2026
946751e
fix(resume): fix the resume checkpoint
Sg312 Apr 1, 2026
ba3bdd0
fix(resume): fix resume client tool
Sg312 Apr 1, 2026
e3f8663
fix(subagents): subagent resume should join on existing subagent text…
Sg312 Apr 1, 2026
e22fccd
improvement(reconnect): harden reconnect logic
Sg312 Apr 1, 2026
86207ee
fix(superagent): fix superagent integration tools
Sg312 Apr 2, 2026
83cf090
improvement(stream): improve stream perf
Sg312 Apr 3, 2026
54266b9
Rebase with origin dev
Sg312 Apr 3, 2026
d7bfe16
fix(tests): fix failing test
Sg312 Apr 3, 2026
8f61262
fix(build): fix type errors
Sg312 Apr 3, 2026
63e9dff
fix(build): fix build errors
Sg312 Apr 3, 2026
2548912
fix(build): fix type errors
Sg312 Apr 3, 2026
7cd4545
feat(mothership): add cli execution
Sg312 Apr 4, 2026
fb12805
fix(mothership): fix function execute tests
Sg312 Apr 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/sim/app/api/billing/update-cost/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { recordUsage } from '@/lib/billing/core/usage-log'
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
import { checkInternalApiKey } from '@/lib/copilot/utils'
import { checkInternalApiKey } from '@/lib/copilot/request/http'
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
import { generateRequestId } from '@/lib/core/utils/request'

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/copilot/api-keys/validate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkServerSideUsageLimits } from '@/lib/billing/calculations/usage-monitor'
import { checkInternalApiKey } from '@/lib/copilot/utils'
import { checkInternalApiKey } from '@/lib/copilot/request/http'

const logger = createLogger('CopilotApiKeysValidate')

Expand Down
33 changes: 22 additions & 11 deletions apps/sim/app/api/copilot/chat/abort/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository'
import { abortActiveStream, waitForPendingChatStream } from '@/lib/copilot/chat-streaming'
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request-helpers'
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http'
import { abortActiveStream } from '@/lib/copilot/request/session/abort'
import { env } from '@/lib/core/config/env'

const logger = createLogger('CopilotChatAbortAPI')
const GO_EXPLICIT_ABORT_TIMEOUT_MS = 3000

export async function POST(request: Request) {
Expand All @@ -15,7 +17,12 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const body = await request.json().catch(() => ({}))
const body = await request.json().catch((err) => {
logger.warn('Abort request body parse failed; continuing with empty object', {
error: err instanceof Error ? err.message : String(err),
})
return {}
})
const streamId = typeof body.streamId === 'string' ? body.streamId : ''
let chatId = typeof body.chatId === 'string' ? body.chatId : ''

Expand All @@ -24,7 +31,13 @@ export async function POST(request: Request) {
}

if (!chatId) {
const run = await getLatestRunForStream(streamId, authenticatedUserId).catch(() => null)
const run = await getLatestRunForStream(streamId, authenticatedUserId).catch((err) => {
logger.warn('getLatestRunForStream failed while resolving chatId for abort', {
streamId,
error: err instanceof Error ? err.message : String(err),
})
return null
})
if (run?.chatId) {
chatId = run.chatId
}
Expand All @@ -50,15 +63,13 @@ export async function POST(request: Request) {
if (!response.ok) {
throw new Error(`Explicit abort marker request failed: ${response.status}`)
}
} catch {
// best effort: local abort should still proceed even if Go marker fails
} catch (err) {
logger.warn('Explicit abort marker request failed; proceeding with local abort', {
streamId,
error: err instanceof Error ? err.message : String(err),
})
}

const aborted = await abortActiveStream(streamId)
if (chatId) {
await waitForPendingChatStream(chatId, GO_EXPLICIT_ABORT_TIMEOUT_MS + 1000, streamId).catch(
() => false
)
}
return NextResponse.json({ aborted })
}
4 changes: 2 additions & 2 deletions apps/sim/app/api/copilot/chat/delete/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ vi.mock('drizzle-orm', () => ({
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
}))

vi.mock('@/lib/copilot/chat-lifecycle', () => ({
vi.mock('@/lib/copilot/chat/lifecycle', () => ({
getAccessibleCopilotChat: mockGetAccessibleCopilotChat,
}))

vi.mock('@/lib/copilot/task-events', () => ({
vi.mock('@/lib/copilot/tasks', () => ({
taskPubSub: { publishStatusChanged: vi.fn() },
}))

Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/copilot/chat/delete/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { getAccessibleCopilotChat } from '@/lib/copilot/chat-lifecycle'
import { taskPubSub } from '@/lib/copilot/task-events'
import { getAccessibleCopilotChat } from '@/lib/copilot/chat/lifecycle'
import { taskPubSub } from '@/lib/copilot/tasks'

const logger = createLogger('DeleteChatAPI')

Expand Down
119 changes: 119 additions & 0 deletions apps/sim/app/api/copilot/chat/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { db } from '@sim/db'
import { copilotChats } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, desc, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getAccessibleCopilotChat } from '@/lib/copilot/chat/lifecycle'
import {
authenticateCopilotRequestSessionOnly,
createBadRequestResponse,
createInternalServerErrorResponse,
createUnauthorizedResponse,
} from '@/lib/copilot/request/http'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('CopilotChatAPI')

function transformChat(chat: {
id: string
title: string | null
model: string | null
messages: unknown
planArtifact?: unknown
config?: unknown
conversationId?: string | null
resources?: unknown
createdAt: Date | null
updatedAt: Date | null
}) {
return {
id: chat.id,
title: chat.title,
model: chat.model,
messages: Array.isArray(chat.messages) ? chat.messages : [],
messageCount: Array.isArray(chat.messages) ? chat.messages.length : 0,
planArtifact: chat.planArtifact || null,
config: chat.config || null,
...('conversationId' in chat ? { activeStreamId: chat.conversationId || null } : {}),
...('resources' in chat
? { resources: Array.isArray(chat.resources) ? chat.resources : [] }
: {}),
createdAt: chat.createdAt,
updatedAt: chat.updatedAt,
}
}

export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const workflowId = searchParams.get('workflowId')
const workspaceId = searchParams.get('workspaceId')
const chatId = searchParams.get('chatId')

const { userId: authenticatedUserId, isAuthenticated } =
await authenticateCopilotRequestSessionOnly()
if (!isAuthenticated || !authenticatedUserId) {
return createUnauthorizedResponse()
}

if (chatId) {
const chat = await getAccessibleCopilotChat(chatId, authenticatedUserId)
if (!chat) {
return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 })
}

logger.info(`Retrieved chat ${chatId}`)
return NextResponse.json({ success: true, chat: transformChat(chat) })
}

if (!workflowId && !workspaceId) {
return createBadRequestResponse('workflowId, workspaceId, or chatId is required')
}

if (workspaceId) {
await assertActiveWorkspaceAccess(workspaceId, authenticatedUserId)
}

if (workflowId) {
const authorization = await authorizeWorkflowByWorkspacePermission({
workflowId,
userId: authenticatedUserId,
action: 'read',
})
if (!authorization.allowed) {
return createUnauthorizedResponse()
}
}

const scopeFilter = workflowId
? eq(copilotChats.workflowId, workflowId)
: eq(copilotChats.workspaceId, workspaceId!)

const chats = await db
.select({
id: copilotChats.id,
title: copilotChats.title,
model: copilotChats.model,
messages: copilotChats.messages,
planArtifact: copilotChats.planArtifact,
config: copilotChats.config,
createdAt: copilotChats.createdAt,
updatedAt: copilotChats.updatedAt,
})
.from(copilotChats)
.where(and(eq(copilotChats.userId, authenticatedUserId), scopeFilter))
.orderBy(desc(copilotChats.updatedAt))

const scope = workflowId ? `workflow ${workflowId}` : `workspace ${workspaceId}`
logger.info(`Retrieved ${chats.length} chats for ${scope}`)

return NextResponse.json({
success: true,
chats: chats.map(transformChat),
})
} catch (error) {
logger.error('Error fetching copilot chats:', error)
return createInternalServerErrorResponse('Failed to fetch chats')
}
}
65 changes: 65 additions & 0 deletions apps/sim/app/api/copilot/chat/rename/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { db } from '@sim/db'
import { copilotChats } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { getAccessibleCopilotChat } from '@/lib/copilot/chat/lifecycle'
import { taskPubSub } from '@/lib/copilot/tasks'

const logger = createLogger('RenameChatAPI')

const RenameChatSchema = z.object({
chatId: z.string().min(1),
title: z.string().min(1).max(200),
})

export async function PATCH(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const { chatId, title } = RenameChatSchema.parse(body)

const chat = await getAccessibleCopilotChat(chatId, session.user.id)
if (!chat) {
return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 })
}

const now = new Date()
const [updated] = await db
.update(copilotChats)
.set({ title, updatedAt: now, lastSeenAt: now })
.where(and(eq(copilotChats.id, chatId), eq(copilotChats.userId, session.user.id)))
.returning({ id: copilotChats.id, workspaceId: copilotChats.workspaceId })

if (!updated) {
return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 })
}

logger.info('Chat renamed', { chatId, title })

if (updated.workspaceId) {
taskPubSub?.publishStatusChanged({
workspaceId: updated.workspaceId,
chatId,
type: 'renamed',
})
}

return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}
logger.error('Error renaming chat:', error)
return NextResponse.json({ success: false, error: 'Failed to rename chat' }, { status: 500 })
}
}
4 changes: 2 additions & 2 deletions apps/sim/app/api/copilot/chat/resources/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
createInternalServerErrorResponse,
createNotFoundResponse,
createUnauthorizedResponse,
} from '@/lib/copilot/request-helpers'
import type { ChatResource, ResourceType } from '@/lib/copilot/resources'
} from '@/lib/copilot/request/http'
import type { ChatResource, ResourceType } from '@/lib/copilot/resources/persistence'

const logger = createLogger('CopilotChatResourcesAPI')

Expand Down
Loading
Loading