Files
simstudioai--sim/apps/sim/lib/copilot/chat/terminal-state.ts
T
wehub-resource-sync d25d482dc2
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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

161 lines
5.5 KiB
TypeScript

import { db } from '@sim/db'
import { copilotChats, copilotMessages } from '@sim/db/schema'
import { and, desc, eq, isNull, sql } from 'drizzle-orm'
import { appendCopilotChatMessages } from '@/lib/copilot/chat/messages-store'
import type { PersistedMessage } from '@/lib/copilot/chat/persisted-message'
import { CopilotChatFinalizeOutcome } from '@/lib/copilot/generated/trace-attribute-values-v1'
import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
import { withCopilotSpan } from '@/lib/copilot/request/otel'
type StreamMarkerPolicy = 'active-only' | 'active-or-cleared'
interface FinalizeAssistantTurnParams {
chatId: string
userMessageId: string
userId?: string
assistantMessage?: PersistedMessage
streamMarkerPolicy?: StreamMarkerPolicy
}
export interface FinalizeAssistantTurnResult {
found: boolean
updated: boolean
appendedAssistant: boolean
workspaceId?: string | null
outcome: (typeof CopilotChatFinalizeOutcome)[keyof typeof CopilotChatFinalizeOutcome]
}
/**
* Clear the active stream marker for a chat and optionally append the assistant
* message if a response has not already been persisted immediately after the
* triggering user message.
*/
export async function finalizeAssistantTurn({
chatId,
userMessageId,
userId,
assistantMessage,
streamMarkerPolicy = 'active-only',
}: FinalizeAssistantTurnParams): Promise<FinalizeAssistantTurnResult> {
return withCopilotSpan(
TraceSpan.CopilotChatFinalizeAssistantTurn,
{
[TraceAttr.DbSystem]: 'postgresql',
[TraceAttr.DbSqlTable]: 'copilot_chats',
[TraceAttr.ChatId]: chatId,
[TraceAttr.ChatUserMessageId]: userMessageId,
[TraceAttr.ChatHasAssistantMessage]: !!assistantMessage,
},
async (span) => {
const result = await db.transaction(async (tx) => {
const where = userId
? and(eq(copilotChats.id, chatId), eq(copilotChats.userId, userId))
: eq(copilotChats.id, chatId)
const [row] = await tx
.select({
conversationId: copilotChats.conversationId,
workspaceId: copilotChats.workspaceId,
model: copilotChats.model,
})
.from(copilotChats)
.where(where)
.for('update')
.limit(1)
if (!row) {
return {
found: false,
updated: false,
appendedAssistant: false,
workspaceId: null,
outcome: CopilotChatFinalizeOutcome.StaleUserMessage,
}
}
const chatModel = row.model ?? null
const markerMatches = row.conversationId === userMessageId
const markerAlreadyCleared = row.conversationId === null
const ownsTurn =
markerMatches || (streamMarkerPolicy === 'active-or-cleared' && markerAlreadyCleared)
if (!ownsTurn) {
return {
found: true,
updated: false,
appendedAssistant: false,
workspaceId: row.workspaceId,
outcome: CopilotChatFinalizeOutcome.StaleUserMessage,
}
}
// Append only when the user message is still the last row: anything
// after it means the turn already has a response (dedup under the lock).
const [lastMessage] = await tx
.select({ messageId: copilotMessages.messageId, role: copilotMessages.role })
.from(copilotMessages)
.where(and(eq(copilotMessages.chatId, chatId), isNull(copilotMessages.deletedAt)))
.orderBy(
sql`${copilotMessages.seq} desc nulls last`,
desc(copilotMessages.createdAt),
desc(copilotMessages.id)
)
.limit(1)
const canAppendAssistant = lastMessage?.messageId === userMessageId
const alreadyHasResponse = lastMessage?.role === 'assistant'
const updateWhere = userId
? and(eq(copilotChats.id, chatId), eq(copilotChats.userId, userId))
: eq(copilotChats.id, chatId)
const baseUpdate = {
conversationId: null,
updatedAt: new Date(),
}
if (assistantMessage && canAppendAssistant) {
await tx.update(copilotChats).set(baseUpdate).where(updateWhere)
await appendCopilotChatMessages(
chatId,
[assistantMessage],
{ streamId: userMessageId, chatModel },
tx
)
return {
found: true,
updated: true,
appendedAssistant: true,
workspaceId: row.workspaceId,
outcome: CopilotChatFinalizeOutcome.AppendedAssistant,
}
}
if (markerMatches) {
await tx.update(copilotChats).set(baseUpdate).where(updateWhere)
return {
found: true,
updated: true,
appendedAssistant: false,
workspaceId: row.workspaceId,
outcome: assistantMessage
? CopilotChatFinalizeOutcome.AssistantAlreadyPersisted
: CopilotChatFinalizeOutcome.ClearedStreamMarkerOnly,
}
}
return {
found: true,
updated: false,
appendedAssistant: false,
workspaceId: row.workspaceId,
outcome: alreadyHasResponse
? CopilotChatFinalizeOutcome.AssistantAlreadyPersisted
: CopilotChatFinalizeOutcome.StaleUserMessage,
}
})
span.setAttribute(TraceAttr.ChatFinalizeOutcome, result.outcome)
return result
}
)
}