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
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:
@@ -0,0 +1,170 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { copilotChatAbortBodySchema } from '@/lib/api/contracts/copilot'
|
||||
import { validationErrorResponse } from '@/lib/api/server'
|
||||
import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository'
|
||||
import { CopilotAbortOutcome } 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 { fetchGo } from '@/lib/copilot/request/go/fetch'
|
||||
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http'
|
||||
import { withCopilotSpan, withIncomingGoSpan } from '@/lib/copilot/request/otel'
|
||||
import {
|
||||
abortActiveStream,
|
||||
releasePendingChatStream,
|
||||
waitForPendingChatStream,
|
||||
} from '@/lib/copilot/request/session'
|
||||
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'
|
||||
|
||||
const logger = createLogger('CopilotChatAbortAPI')
|
||||
const GO_EXPLICIT_ABORT_TIMEOUT_MS = 3000
|
||||
const STREAM_ABORT_SETTLE_TIMEOUT_MS = 8000
|
||||
|
||||
// POST /api/copilot/chat/abort — fires on user Stop; marks the Go
|
||||
// side aborted then waits for the prior stream to settle.
|
||||
export const POST = withRouteHandler((request: NextRequest) =>
|
||||
withIncomingGoSpan(
|
||||
request.headers,
|
||||
TraceSpan.CopilotChatAbortStream,
|
||||
undefined,
|
||||
async (rootSpan) => {
|
||||
const { userId: authenticatedUserId, isAuthenticated } =
|
||||
await authenticateCopilotRequestSessionOnly()
|
||||
|
||||
if (!isAuthenticated || !authenticatedUserId) {
|
||||
rootSpan.setAttribute(TraceAttr.CopilotAbortOutcome, CopilotAbortOutcome.Unauthorized)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json().catch((err) => {
|
||||
logger.warn('Abort request body parse failed; continuing with empty object', {
|
||||
error: getErrorMessage(err),
|
||||
})
|
||||
return {}
|
||||
})
|
||||
const validation = copilotChatAbortBodySchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
rootSpan.setAttribute(TraceAttr.CopilotAbortOutcome, CopilotAbortOutcome.MissingStreamId)
|
||||
return validationErrorResponse(validation.error, 'Invalid request body')
|
||||
}
|
||||
const { streamId, chatId: parsedChatId } = validation.data
|
||||
let chatId = parsedChatId
|
||||
|
||||
if (!streamId) {
|
||||
rootSpan.setAttribute(TraceAttr.CopilotAbortOutcome, CopilotAbortOutcome.MissingStreamId)
|
||||
return NextResponse.json({ error: 'streamId is required' }, { status: 400 })
|
||||
}
|
||||
rootSpan.setAttributes({
|
||||
[TraceAttr.StreamId]: streamId,
|
||||
[TraceAttr.UserId]: authenticatedUserId,
|
||||
})
|
||||
|
||||
const run = await getLatestRunForStream(streamId, authenticatedUserId).catch((err) => {
|
||||
logger.warn('getLatestRunForStream failed while resolving abort context', {
|
||||
streamId,
|
||||
error: getErrorMessage(err),
|
||||
})
|
||||
return null
|
||||
})
|
||||
if (!chatId && run?.chatId) {
|
||||
chatId = run.chatId
|
||||
}
|
||||
const workspaceId = run?.workspaceId ?? undefined
|
||||
if (chatId) rootSpan.setAttribute(TraceAttr.ChatId, chatId)
|
||||
|
||||
const aborted = await abortActiveStream(streamId)
|
||||
rootSpan.setAttribute(TraceAttr.CopilotAbortLocalAborted, aborted)
|
||||
|
||||
let goAbortOk = false
|
||||
try {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (env.COPILOT_API_KEY) {
|
||||
headers['x-api-key'] = env.COPILOT_API_KEY
|
||||
}
|
||||
Object.assign(headers, getMothershipSourceEnvHeaders())
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(
|
||||
() => controller.abort('timeout:go_explicit_abort_fetch'),
|
||||
GO_EXPLICIT_ABORT_TIMEOUT_MS
|
||||
)
|
||||
const mothershipBaseURL = await getMothershipBaseURL({ userId: authenticatedUserId })
|
||||
const response = await fetchGo(`${mothershipBaseURL}/api/streams/explicit-abort`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
messageId: streamId,
|
||||
userId: authenticatedUserId,
|
||||
...(chatId ? { chatId } : {}),
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
}),
|
||||
spanName: 'sim → go /api/streams/explicit-abort',
|
||||
operation: 'explicit_abort',
|
||||
attributes: {
|
||||
[TraceAttr.StreamId]: streamId,
|
||||
...(chatId ? { [TraceAttr.ChatId]: chatId } : {}),
|
||||
},
|
||||
}).finally(() => clearTimeout(timeout))
|
||||
if (!response.ok) {
|
||||
throw new Error(`Explicit abort marker request failed: ${response.status}`)
|
||||
}
|
||||
goAbortOk = true
|
||||
} catch (err) {
|
||||
logger.warn('Explicit abort marker request failed after local abort', {
|
||||
streamId,
|
||||
error: getErrorMessage(err),
|
||||
})
|
||||
}
|
||||
rootSpan.setAttribute(TraceAttr.CopilotAbortGoMarkerOk, goAbortOk)
|
||||
|
||||
if (chatId) {
|
||||
const settled = await withCopilotSpan(
|
||||
TraceSpan.CopilotChatAbortWaitSettle,
|
||||
{
|
||||
[TraceAttr.ChatId]: chatId,
|
||||
[TraceAttr.StreamId]: streamId,
|
||||
[TraceAttr.SettleTimeoutMs]: STREAM_ABORT_SETTLE_TIMEOUT_MS,
|
||||
},
|
||||
async (settleSpan) => {
|
||||
const start = Date.now()
|
||||
const ok = await waitForPendingChatStream(
|
||||
chatId,
|
||||
STREAM_ABORT_SETTLE_TIMEOUT_MS,
|
||||
streamId
|
||||
)
|
||||
settleSpan.setAttributes({
|
||||
[TraceAttr.SettleWaitMs]: Date.now() - start,
|
||||
[TraceAttr.SettleCompleted]: ok,
|
||||
})
|
||||
return ok
|
||||
}
|
||||
)
|
||||
if (!settled) {
|
||||
// The holder didn't settle within the grace window even though the
|
||||
// user explicitly stopped it and abort markers are written on both
|
||||
// sides (local + Go). Don't leave the chat hostage to a wedged
|
||||
// handler: break its stream lock. This is safe by construction —
|
||||
// releaseLock only deletes when the value still matches this
|
||||
// streamId (never clobbers a newer stream), and the old handler's
|
||||
// heartbeat uses extendLock-if-owner, so it observes the loss and
|
||||
// stops heartbeating rather than re-asserting.
|
||||
await releasePendingChatStream(chatId, streamId)
|
||||
logger.warn('Stream did not settle after abort; force-released chat stream lock', {
|
||||
chatId,
|
||||
streamId,
|
||||
})
|
||||
rootSpan.setAttribute(TraceAttr.CopilotAbortOutcome, CopilotAbortOutcome.ForceReleased)
|
||||
return NextResponse.json({ aborted, settled: false, forceReleased: true })
|
||||
}
|
||||
rootSpan.setAttribute(TraceAttr.CopilotAbortOutcome, CopilotAbortOutcome.Settled)
|
||||
return NextResponse.json({ aborted, settled: true })
|
||||
}
|
||||
|
||||
rootSpan.setAttribute(TraceAttr.CopilotAbortOutcome, CopilotAbortOutcome.NoChatId)
|
||||
return NextResponse.json({ aborted })
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user