import { type MachinePresetName, conditionallyImportPacket, parsePacket, stringifyIO, } from "@trigger.dev/core/v3"; import { type TaskRun } from "@trigger.dev/database"; import { findEnvironmentById } from "~/models/runtimeEnvironment.server"; import { baseWorkerQueue } from "~/runEngine/concerns/workerQueueSplit.server"; import { logger } from "~/services/logger.server"; import { BaseService } from "./baseService.server"; import { OutOfEntitlementError, TriggerTaskService } from "./triggerTask.server"; import { type RunOptionsData } from "../testTask"; import { replaceSuperJsonPayload } from "@trigger.dev/core/v3/utils/ioSerialization"; import { determineRealtimeStreamsVersion } from "~/services/realtime/v1StreamsGlobal.server"; type OverrideOptions = { environmentId?: string; payload?: string; metadata?: unknown; bulkActionId?: string; triggerSource?: string; } & RunOptionsData; export class ReplayTaskRunService extends BaseService { public async call(existingTaskRun: TaskRun, overrideOptions: OverrideOptions = {}) { // An override environment must belong to the same project as the source // run. The source project is derived from the run's own environment rather // than existingTaskRun.projectId, since the buffered-run fallback passes a // synthetic TaskRun with no projectId. Only check when a distinct override // is supplied; otherwise the run's own environment is used. if ( overrideOptions.environmentId && overrideOptions.environmentId !== existingTaskRun.runtimeEnvironmentId ) { const [overrideEnvironment, sourceEnvironment] = await Promise.all([ this._prisma.runtimeEnvironment.findFirst({ where: { id: overrideOptions.environmentId }, select: { projectId: true }, }), this._prisma.runtimeEnvironment.findFirst({ where: { id: existingTaskRun.runtimeEnvironmentId }, select: { projectId: true }, }), ]); if ( !overrideEnvironment || !sourceEnvironment || overrideEnvironment.projectId !== sourceEnvironment.projectId ) { logger.warn("Refusing to replay a run into an environment outside its project", { taskRunId: existingTaskRun.id, taskRunFriendlyId: existingTaskRun.friendlyId, sourceEnvironmentId: existingTaskRun.runtimeEnvironmentId, sourceProjectId: sourceEnvironment?.projectId ?? null, overrideEnvironmentId: overrideOptions.environmentId, overrideProjectId: overrideEnvironment?.projectId ?? null, }); throw new Error("Cannot replay a run into an environment outside its project"); } } const authenticatedEnvironment = await findEnvironmentById( overrideOptions.environmentId ?? existingTaskRun.runtimeEnvironmentId ); if (!authenticatedEnvironment) { return; } if (authenticatedEnvironment.archivedAt) { throw new Error("Can't replay a run on an archived environment"); } logger.info("Replaying task run", { taskRunId: existingTaskRun.id, taskRunFriendlyId: existingTaskRun.friendlyId, }); const existingEnvironment = await this._prisma.runtimeEnvironment.findFirstOrThrow({ where: { id: existingTaskRun.runtimeEnvironmentId, }, select: { id: true, type: true, }, }); const payloadPacket = await this.overrideExistingPayloadPacket( existingTaskRun, overrideOptions.payload ); const parsedPayload = payloadPacket.dataType === "application/json" ? await parsePacket(payloadPacket) : payloadPacket.data; const payloadType = payloadPacket.dataType; const metadata = overrideOptions.metadata ?? (await this.getExistingMetadata(existingTaskRun)); const tags = overrideOptions.tags ?? existingTaskRun.runTags; // Only use the region from the existing run if V2 engine and neither environment is dev const ignoreRegion = existingTaskRun.engine === "V1" || existingEnvironment.type === "DEVELOPMENT" || authenticatedEnvironment.type === "DEVELOPMENT"; const region = ignoreRegion ? undefined : overrideOptions.region || existingTaskRun.region || baseWorkerQueue(existingTaskRun.workerQueue); try { const taskQueue = await this._prisma.taskQueue.findFirst({ where: { runtimeEnvironmentId: authenticatedEnvironment.id, name: overrideOptions.queue ?? existingTaskRun.queue, }, }); const triggerTaskService = new TriggerTaskService(); const result = await triggerTaskService.call( existingTaskRun.taskIdentifier, authenticatedEnvironment, { payload: parsedPayload, options: { payloadType, queue: taskQueue ? { name: taskQueue.name, } : undefined, test: existingTaskRun.isTest, tags, metadata: metadata, delay: overrideOptions.delaySeconds ? new Date(Date.now() + overrideOptions.delaySeconds * 1000) : undefined, ttl: overrideOptions.ttlSeconds, idempotencyKey: overrideOptions.idempotencyKey, idempotencyKeyTTL: overrideOptions.idempotencyKeyTTLSeconds ? `${overrideOptions.idempotencyKeyTTLSeconds}s` : undefined, concurrencyKey: overrideOptions.concurrencyKey ?? existingTaskRun.concurrencyKey ?? undefined, maxAttempts: overrideOptions.maxAttempts, maxDuration: overrideOptions.maxDurationSeconds, machine: overrideOptions.machine ?? (existingTaskRun.machinePreset as MachinePresetName) ?? undefined, lockToVersion: overrideOptions.version === "latest" ? undefined : overrideOptions.version, bulkActionId: overrideOptions?.bulkActionId, region, priority: overrideOptions.prioritySeconds, }, }, { spanParentAsLink: true, parentAsLinkType: "replay", replayedFromTaskRunFriendlyId: existingTaskRun.friendlyId, traceContext: { traceparent: `00-${existingTaskRun.traceId}-${existingTaskRun.spanId}-01`, }, realtimeStreamsVersion: determineRealtimeStreamsVersion( existingTaskRun.realtimeStreamsVersion ), triggerSource: overrideOptions.triggerSource ?? "api", triggerAction: "replay", } ); return result?.run; } catch (error) { if (error instanceof OutOfEntitlementError) { return; } logger.error("Failed to replay a run", { error: error instanceof Error ? error.message : error, }); return; } } private async overrideExistingPayloadPacket( existingTaskRun: TaskRun, stringifiedPayloadOverride: string | undefined ) { if (existingTaskRun.payloadType === "application/store") { return conditionallyImportPacket({ data: existingTaskRun.payload, dataType: existingTaskRun.payloadType, }); } if (stringifiedPayloadOverride && existingTaskRun.payloadType === "application/super+json") { const newPayload = await replaceSuperJsonPayload( existingTaskRun.payload, stringifiedPayloadOverride ); return stringifyIO(newPayload); } return conditionallyImportPacket({ data: stringifiedPayloadOverride ?? existingTaskRun.payload, dataType: existingTaskRun.payloadType, }); } private async getExistingMetadata(existingTaskRun: TaskRun) { if (!existingTaskRun.seedMetadata) { return undefined; } return parsePacket({ data: existingTaskRun.seedMetadata, dataType: existingTaskRun.seedMetadataType, }); } }