Files
2026-07-13 13:32:57 +08:00

1093 lines
31 KiB
TypeScript

import {
type MachinePreset,
prettyPrintPacket,
RunAnnotations,
SemanticInternalAttributes,
type TaskRunContext,
TaskRunError,
TriggerTraceContext,
type V3TaskRunContext,
} from "@trigger.dev/core/v3";
import { AttemptId, getMaxDuration, parseTraceparent } from "@trigger.dev/core/v3/isomorphic";
import { $replica, runOpsNewReplica, runOpsSplitReadEnabled } from "~/db.server";
import {
extractIdempotencyKeyScope,
getUserProvidedIdempotencyKey,
} from "@trigger.dev/core/v3/serverOnly";
import {
extractAIEmbedData,
extractAISpanData,
extractAISummarySpanData,
extractAIToolCallData,
} from "~/components/runs/v3/ai";
import { RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
import { baseWorkerQueue } from "~/runEngine/concerns/workerQueueSplit.server";
import { logger } from "~/services/logger.server";
import { safeJsonParse } from "~/utils/json";
import { rehydrateAttribute } from "~/v3/eventRepository/eventRepository.server";
import type { IEventRepository } from "~/v3/eventRepository/eventRepository.types";
import { getEventRepositoryForStore } from "~/v3/eventRepository/index.server";
import { machinePresetFromRun } from "~/v3/machinePresets.server";
import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.server";
import { buildSyntheticSpanRun } from "~/v3/mollifier/syntheticSpanRun.server";
import { engine } from "~/v3/runEngine.server";
import { runStore } from "~/v3/runStore.server";
import { getTaskEventStoreTableForRun, type TaskEventStoreTable } from "~/v3/taskEventStore.server";
import { isFailedRunStatus, isFinalRunStatus } from "~/v3/taskStatus";
import { BasePresenter } from "./basePresenter.server";
import { WaitpointPresenter } from "./WaitpointPresenter.server";
import {
controlPlaneResolver,
type ResolvedRunLockedWorker,
} from "~/v3/runOpsMigration/controlPlaneResolver.server";
import type { AuthenticatedEnvironment } from "@trigger.dev/core/v3/auth/environment";
export type PromptSpanData = {
slug: string;
version: number;
labels: string;
model?: string;
template?: string;
text?: string;
input?: string;
config?: string;
};
function extractPromptSpanData(properties: Record<string, unknown>): PromptSpanData | undefined {
// Properties come as an unflattened nested object from ClickHouse,
// e.g. { prompt: { slug: "...", version: 3, ... } }
const prompt = properties.prompt;
if (!prompt || typeof prompt !== "object") {
return undefined;
}
const p = prompt as Record<string, unknown>;
const slug = p.slug;
const version = p.version;
if (typeof slug !== "string" || typeof version !== "number") {
return undefined;
}
return {
slug,
version,
labels: typeof p.labels === "string" ? p.labels : "",
model: typeof p.model === "string" ? p.model : undefined,
template: typeof p.template === "string" ? p.template : undefined,
text: typeof p.text === "string" ? p.text : undefined,
input: typeof p.input === "string" ? p.input : undefined,
config: typeof p.config === "string" ? p.config : undefined,
};
}
// Grounded in `getRun` (the canonical shape source), not inferred from `call`:
// `call`'s buffered branch returns `buildSyntheticSpanRun` which is annotated
// `Promise<SpanRun>`, so deriving SpanRun from `call` would be a circular type
// reference TS rejects. `getRun` doesn't recurse, breaking the cycle.
export type SpanRun = NonNullable<
Awaited<ReturnType<InstanceType<typeof SpanPresenter>["getRun"]>>
>;
type Result = Awaited<ReturnType<SpanPresenter["call"]>>;
export type Span = NonNullable<NonNullable<Result>["span"]>;
type FindRunResult = NonNullable<
Awaited<ReturnType<InstanceType<typeof SpanPresenter>["findRun"]>>
>;
// Run-ops TaskRun reads (parent run in `call`, hydrate in `findRun`, children in
// `#getSpan`) go through the `runStore` seam; split routing is the RoutingRunStore's
// job below it. Control-plane reads stay on `this._replica`/`this._prisma`.
export class SpanPresenter extends BasePresenter {
public async call({
userId,
projectSlug,
envSlug,
spanId,
runFriendlyId,
linkedRunId,
}: {
userId: string;
projectSlug: string;
// Optional for backwards compatibility, required for the mollifier
// buffer fallback when the parent run isn't yet in PG — we need to
// resolve the env id to satisfy `findRunByIdWithMollifierFallback`'s
// auth check.
envSlug?: string;
spanId: string;
runFriendlyId: string;
linkedRunId?: string;
}) {
const project = await this._replica.project.findFirst({
where: {
slug: projectSlug,
organization: {
members: {
some: {
userId,
},
},
},
},
});
if (!project) {
throw new Error("Project not found");
}
const parentRun = await runStore.findRun(
{
friendlyId: runFriendlyId,
projectId: project.id,
},
{
select: {
traceId: true,
runtimeEnvironmentId: true,
projectId: true,
taskEventStore: true,
createdAt: true,
completedAt: true,
},
},
this._prisma
);
if (!parentRun) {
// PG miss → fall back to the mollifier buffer. Without this the
// right-side span detail panel on the run-detail page never
// resolves for buffered runs: `call()` returns undefined, the
// resource route redirects with an "Event not found" toast, the
// run-detail page reloads, the toast fires again — a perpetual
// spin until the drainer materialises the row. Synthesise a
// SpanRun straight from the buffer snapshot, reusing
// `buildSyntheticSpanRun` (the same helper the run-detail
// loader's header fallback already uses).
if (!envSlug) return;
const envRow = await this._replica.runtimeEnvironment.findFirst({
where: { project: { id: project.id }, slug: envSlug },
select: { id: true, slug: true, type: true, organizationId: true },
});
if (!envRow) return;
const buffered = await findRunByIdWithMollifierFallback({
runId: runFriendlyId,
environmentId: envRow.id,
organizationId: envRow.organizationId,
});
if (!buffered) return;
const synth = await buildSyntheticSpanRun({
run: buffered,
environment: { id: envRow.id, slug: envRow.slug, type: envRow.type },
});
return { type: "run" as const, run: synth };
}
const { traceId } = parentRun;
const repository = await getEventRepositoryForStore(
parentRun.taskEventStore,
project.organizationId
);
const eventStore = getTaskEventStoreTableForRun(parentRun);
const run = await this.getRun({
eventStore,
traceId,
eventRepository: repository,
spanId,
linkedRunId,
createdAt: parentRun.createdAt,
completedAt: parentRun.completedAt,
environmentId: parentRun.runtimeEnvironmentId,
});
if (run) {
return {
type: "run" as const,
run,
};
}
const span = await this.#getSpan({
eventStore,
spanId,
traceId,
environmentId: parentRun.runtimeEnvironmentId,
projectId: parentRun.projectId,
createdAt: parentRun.createdAt,
completedAt: parentRun.completedAt,
eventRepository: repository,
});
if (!span) {
throw new Error("Span not found");
}
return {
type: "span" as const,
span,
};
}
async getRun({
eventStore,
environmentId,
traceId,
eventRepository,
spanId,
linkedRunId,
createdAt,
completedAt,
}: {
eventStore: TaskEventStoreTable;
environmentId: string;
traceId: string;
eventRepository: IEventRepository;
spanId: string;
linkedRunId?: string;
createdAt: Date;
completedAt: Date | null;
}) {
// Use linkedRunId if provided (for cached spans), otherwise look up by spanId
const run = await this.findRun({ originalRunId: linkedRunId, spanId, environmentId });
if (!run) {
return;
}
const environment = await controlPlaneResolver.resolveAuthenticatedEnv(
run.runtimeEnvironmentId
);
if (!environment) {
return undefined;
}
const lockedWorker = await controlPlaneResolver.resolveRunLockedWorker({
lockedById: run.lockedById,
lockedToVersionId: run.lockedToVersionId,
});
const isFinished = isFinalRunStatus(run.status);
const output = !isFinished
? undefined
: run.outputType === "application/store"
? `/resources/packets/${environment.id}/${run.output}`
: typeof run.output !== "undefined" && run.output !== null
? await prettyPrintPacket(run.output, run.outputType ?? undefined)
: undefined;
const payload =
run.payloadType === "application/store"
? `/resources/packets/${environment.id}/${run.payload}`
: typeof run.payload !== "undefined" && run.payload !== null
? await prettyPrintPacket(run.payload, run.payloadType ?? undefined)
: undefined;
let error: TaskRunError | undefined = undefined;
if (run?.error) {
const result = TaskRunError.safeParse(run.error);
if (result.success) {
error = result.data;
} else {
error = {
type: "CUSTOM_ERROR",
raw: JSON.stringify(run.error),
};
}
}
const metadata = run.metadata
? await prettyPrintPacket(run.metadata, run.metadataType, {
filteredKeys: ["$$streams", "$$streamsVersion", "$$streamsBaseUrl"],
})
: undefined;
const machine = run.machinePreset ? machinePresetFromRun(run) : undefined;
const context = await this.#getTaskRunContext({
run,
machine: machine ?? undefined,
environment,
lockedWorker,
});
const externalTraceId = this.#getExternalTraceId(run.traceContext);
const taskKind = RunAnnotations.safeParse(run.annotations).data?.taskKind;
const isAgentRun = taskKind === "AGENT";
const isScheduled = taskKind === "SCHEDULED";
let region: { name: string; location: string | null } | null = null;
if (environment.type !== "DEVELOPMENT" && run.engine !== "V1") {
const workerGroup = await this._replica.workerInstanceGroup.findFirst({
select: {
name: true,
location: true,
},
where: {
// masterQueue is unique and IS the run's backing queue, so this finds
// the group the run actually ran on.
masterQueue: baseWorkerQueue(run.workerQueue),
},
});
// Show the stamped geo region as the name so a migrated run never reveals
// its compute backing; fall back to the group name for unstamped runs.
region = workerGroup
? { name: run.region ?? workerGroup.name, location: workerGroup.location }
: null;
}
// Only AGENT-tagged runs can be session-bound, so skip the SessionRun lookup
// for the much larger set of standard runs — the cheapest query is the one we
// don't run.
const sessionRun = isAgentRun
? await this._replica.sessionRun.findFirst({
where: { runId: run.id },
select: {
reason: true,
triggeredAt: true,
session: {
select: {
friendlyId: true,
externalId: true,
type: true,
taskIdentifier: true,
closedAt: true,
expiresAt: true,
},
},
},
})
: null;
const session = sessionRun
? {
friendlyId: sessionRun.session.friendlyId,
externalId: sessionRun.session.externalId,
type: sessionRun.session.type,
taskIdentifier: sessionRun.session.taskIdentifier,
status:
sessionRun.session.closedAt != null
? ("CLOSED" as const)
: sessionRun.session.expiresAt != null &&
sessionRun.session.expiresAt.getTime() < Date.now()
? ("EXPIRED" as const)
: ("ACTIVE" as const),
reason: sessionRun.reason,
triggeredAt: sessionRun.triggeredAt,
}
: undefined;
return {
id: run.id,
friendlyId: run.friendlyId,
status: run.status,
statusReason: run.statusReason ?? undefined,
createdAt: run.createdAt,
startedAt: run.startedAt,
executedAt: run.executedAt,
updatedAt: run.updatedAt,
delayUntil: run.delayUntil,
expiredAt: run.expiredAt,
completedAt: run.completedAt,
logsDeletedAt: run.logsDeletedAt,
ttl: run.ttl,
taskIdentifier: run.taskIdentifier,
version: lockedWorker?.lockedToVersion?.version,
sdkVersion: lockedWorker?.lockedToVersion?.sdkVersion,
runtime: lockedWorker?.lockedToVersion?.runtime,
runtimeVersion: lockedWorker?.lockedToVersion?.runtimeVersion,
isTest: run.isTest,
replayedFromTaskRunFriendlyId: run.replayedFromTaskRunFriendlyId,
environmentId: environment.id,
idempotencyKey: getUserProvidedIdempotencyKey(run),
idempotencyKeyExpiresAt: run.idempotencyKeyExpiresAt,
idempotencyKeyScope: extractIdempotencyKeyScope(run),
idempotencyKeyStatus: this.getIdempotencyKeyStatus(run),
debounce: run.debounce as { key: string; delay: string; createdAt: Date } | null,
schedule: await this.resolveSchedule(run.scheduleId ?? undefined),
queue: {
name: run.queue,
isCustomQueue: !run.queue.startsWith("task/"),
concurrencyKey: run.concurrencyKey,
},
tags: run.runTags,
baseCostInCents: run.baseCostInCents,
costInCents: run.costInCents,
totalCostInCents: run.costInCents + run.baseCostInCents,
usageDurationMs: run.usageDurationMs,
isFinished,
isRunning: RUNNING_STATUSES.includes(run.status),
isError: isFailedRunStatus(run.status),
isAgentRun,
isScheduled,
payload,
payloadType: run.payloadType,
output,
outputType: run?.outputType ?? "application/json",
error,
relationships: {
root: run.rootTaskRun
? {
...run.rootTaskRun,
isParent: run.parentTaskRun?.friendlyId === run.rootTaskRun.friendlyId,
}
: undefined,
parent: run.parentTaskRun ?? undefined,
},
context: JSON.stringify(context, null, 2),
metadata,
maxDurationInSeconds: getMaxDuration(run.maxDurationInSeconds),
batch: run.batch ? { friendlyId: run.batch.friendlyId } : undefined,
session,
engine: run.engine,
region,
workerQueue: run.workerQueue,
traceId: run.traceId,
spanId: run.spanId,
isCached: !!linkedRunId,
isBuffered: false,
machinePreset: machine?.name,
taskEventStore: run.taskEventStore,
externalTraceId,
};
}
private getIdempotencyKeyStatus(run: {
idempotencyKey: string | null;
idempotencyKeyExpiresAt: Date | null;
idempotencyKeyOptions: unknown;
}): "active" | "inactive" | "expired" | undefined {
// No idempotency configured if no scope exists
const scope = extractIdempotencyKeyScope(run);
if (!scope) {
return undefined;
}
// Check if expired first (takes precedence)
if (run.idempotencyKeyExpiresAt && run.idempotencyKeyExpiresAt < new Date()) {
return "expired";
}
// Check if reset (hash is null but options exist)
if (run.idempotencyKey === null) {
return "inactive";
}
return "active";
}
async resolveSchedule(scheduleId?: string) {
if (!scheduleId) {
return;
}
const schedule = await this._replica.taskSchedule.findFirst({
where: {
id: scheduleId,
},
select: {
friendlyId: true,
generatorExpression: true,
timezone: true,
generatorDescription: true,
},
});
if (!schedule) {
return;
}
return {
friendlyId: schedule.friendlyId,
generatorExpression: schedule.generatorExpression,
description: schedule.generatorDescription,
timezone: schedule.timezone,
};
}
async findRun({
originalRunId,
spanId,
environmentId,
}: {
originalRunId?: string;
spanId: string;
environmentId: string;
}) {
const run = await runStore.findRun(
originalRunId
? {
friendlyId: originalRunId,
runtimeEnvironmentId: environmentId,
}
: {
spanId,
runtimeEnvironmentId: environmentId,
},
{
select: {
id: true,
runtimeEnvironmentId: true,
lockedById: true,
lockedToVersionId: true,
spanId: true,
traceId: true,
traceContext: true,
//metadata
number: true,
taskIdentifier: true,
friendlyId: true,
isTest: true,
maxDurationInSeconds: true,
taskEventStore: true,
runTags: true,
machinePreset: true,
engine: true,
workerQueue: true,
region: true,
error: true,
output: true,
outputType: true,
//status + duration
status: true,
statusReason: true,
startedAt: true,
executedAt: true,
createdAt: true,
updatedAt: true,
queuedAt: true,
completedAt: true,
logsDeletedAt: true,
//idempotency
idempotencyKey: true,
idempotencyKeyExpiresAt: true,
idempotencyKeyOptions: true,
//debounce
debounce: true,
//delayed
delayUntil: true,
//ttl
ttl: true,
expiredAt: true,
//queue
queue: true,
concurrencyKey: true,
//schedule
scheduleId: true,
//usage
baseCostInCents: true,
costInCents: true,
usageDurationMs: true,
payload: true,
payloadType: true,
metadata: true,
metadataType: true,
annotations: true,
maxAttempts: true,
//relationships
rootTaskRun: {
select: {
taskIdentifier: true,
friendlyId: true,
spanId: true,
createdAt: true,
},
},
parentTaskRun: {
select: {
taskIdentifier: true,
friendlyId: true,
spanId: true,
},
},
batch: {
select: {
friendlyId: true,
},
},
replayedFromTaskRunFriendlyId: true,
attempts: {
take: 1,
orderBy: {
createdAt: "desc",
},
select: {
number: true,
status: true,
createdAt: true,
friendlyId: true,
},
},
},
},
this._replica
);
return run;
}
async #getSpan({
eventStore,
eventRepository,
traceId,
spanId,
environmentId,
projectId,
createdAt,
completedAt,
}: {
eventRepository: IEventRepository;
traceId: string;
spanId: string;
environmentId: string;
projectId: string;
eventStore: TaskEventStoreTable;
createdAt: Date;
completedAt: Date | null;
}) {
const span = await eventRepository.getSpan(
eventStore,
environmentId,
spanId,
traceId,
createdAt,
completedAt ?? undefined,
{ includeDebugLogs: true }
);
if (!span) {
return;
}
const triggeredRuns = await runStore.findRuns(
{
where: {
parentSpanId: spanId,
},
select: {
friendlyId: true,
taskIdentifier: true,
spanId: true,
createdAt: true,
status: true,
},
},
this._replica
);
const data = {
spanId: span.spanId,
parentId: span.parentId,
message: span.message,
isError: span.isError,
isPartial: span.isPartial,
isCancelled: span.isCancelled,
level: span.level,
startTime: span.startTime,
duration: span.duration,
events: span.events,
style: span.style,
properties:
span.properties &&
typeof span.properties === "object" &&
Object.keys(span.properties).length > 0
? JSON.stringify(span.properties, null, 2)
: undefined,
resourceProperties:
span.resourceProperties &&
typeof span.resourceProperties === "object" &&
Object.keys(span.resourceProperties).length > 0
? JSON.stringify(span.resourceProperties, null, 2)
: undefined,
entity: span.entity,
metadata: span.metadata,
triggeredRuns,
aiData:
span.properties && typeof span.properties === "object"
? extractAISpanData(span.properties as Record<string, unknown>, span.duration / 1_000_000)
: undefined,
};
switch (span.entity.type) {
case "waitpoint": {
if (!span.entity.id) {
logger.error(`SpanPresenter: No waitpoint id`, {
spanId,
waitpointFriendlyId: span.entity.id,
});
return { ...data, entity: null };
}
const presenter = new WaitpointPresenter(undefined, undefined, {
newClient: runOpsNewReplica,
legacyReplica: $replica,
splitEnabled: runOpsSplitReadEnabled,
});
const waitpoint = await presenter.call({
friendlyId: span.entity.id,
environmentId,
projectId,
});
if (!waitpoint) {
logger.error(`SpanPresenter: Waitpoint not found`, {
spanId,
waitpointFriendlyId: span.entity.id,
});
return { ...data, entity: null };
}
return {
...data,
entity: {
type: "waitpoint" as const,
object: waitpoint,
},
};
}
case "attempt": {
const isWarmStart = rehydrateAttribute<boolean>(
span.metadata,
SemanticInternalAttributes.WARM_START
);
return {
...data,
entity: {
type: "attempt" as const,
object: {
isWarmStart,
},
},
};
}
case "realtime-stream": {
if (!span.entity.id) {
logger.error(`SpanPresenter: No realtime stream id`, {
spanId,
realtimeStreamId: span.entity.id,
});
return { ...data, entity: null };
}
const [runId, streamKey] = span.entity.id.split(":");
if (!runId || !streamKey) {
logger.error(`SpanPresenter: Invalid realtime stream id`, {
spanId,
realtimeStreamId: span.entity.id,
});
return { ...data, entity: null };
}
const metadata = span.entity.metadata
? (safeJsonParse(span.entity.metadata) as Record<string, unknown> | undefined)
: undefined;
return {
...data,
entity: {
type: "realtime-stream" as const,
object: {
runId,
streamKey,
metadata,
},
},
};
}
case "input-stream": {
if (!span.entity.id) {
logger.error(`SpanPresenter: No input stream id`, {
spanId,
inputStreamId: span.entity.id,
});
return { ...data, entity: null };
}
const [runId, streamId] = span.entity.id.split(":");
if (!runId || !streamId) {
logger.error(`SpanPresenter: Invalid input stream id`, {
spanId,
inputStreamId: span.entity.id,
});
return { ...data, entity: null };
}
// Translate user-facing stream ID to internal S2 stream name
const s2StreamKey = `$trigger.input:${streamId}`;
return {
...data,
entity: {
type: "realtime-stream" as const,
object: {
runId,
streamKey: s2StreamKey,
displayName: streamId,
metadata: undefined,
},
},
};
}
case "prompt": {
const promptData = extractPromptSpanData(span.properties as Record<string, unknown>);
if (promptData) {
return {
...data,
entity: { type: "prompt" as const, object: promptData },
};
}
return { ...data, entity: null };
}
default: {
// Check for top-level AI SDK parent spans by message name
const AI_SUMMARY_MESSAGES = [
"ai.generateText",
"ai.streamText",
"ai.generateObject",
"ai.streamObject",
];
if (typeof span.message === "string" && AI_SUMMARY_MESSAGES.includes(span.message)) {
const aiSummaryData = extractAISummarySpanData(
span.properties as Record<string, unknown>,
span.duration / 1_000_000
);
if (aiSummaryData) {
const promptVersionData = await this.#lookupPromptVersion(
aiSummaryData.promptSlug,
aiSummaryData.promptVersion,
aiSummaryData.promptLabels,
aiSummaryData.promptInput,
projectId,
environmentId
);
return {
...data,
entity: {
type: "ai-summary" as const,
object: aiSummaryData,
promptVersionData,
},
};
}
}
if (span.message === "ai.toolCall") {
const toolCallData = extractAIToolCallData(
span.properties as Record<string, unknown>,
span.duration / 1_000_000
);
if (toolCallData) {
return {
...data,
entity: { type: "ai-tool-call" as const, object: toolCallData },
};
}
}
if (span.message === "ai.embed") {
const embedData = extractAIEmbedData(
span.properties as Record<string, unknown>,
span.duration / 1_000_000
);
if (embedData) {
return {
...data,
entity: { type: "ai-embed" as const, object: embedData },
};
}
}
// Child generation spans (doGenerate/doStream) with gen_ai.* attributes
if (data.aiData) {
const promptVersionData = await this.#lookupPromptVersion(
data.aiData.promptSlug,
data.aiData.promptVersion,
data.aiData.promptLabels,
data.aiData.promptInput,
projectId,
environmentId
);
return {
...data,
entity: {
type: "ai-generation" as const,
object: data.aiData,
promptVersionData,
},
};
}
return { ...data, entity: null };
}
}
}
async #lookupPromptVersion(
promptSlug: string | undefined,
promptVersion: string | undefined,
promptLabels: string | undefined,
promptInput: string | undefined,
projectId: string,
environmentId: string
): Promise<PromptSpanData | undefined> {
if (!promptSlug || !promptVersion) return undefined;
const prompt = await this._replica.prompt.findUnique({
where: {
projectId_runtimeEnvironmentId_slug: {
projectId,
runtimeEnvironmentId: environmentId,
slug: promptSlug,
},
},
});
if (!prompt) return undefined;
const version = await this._replica.promptVersion.findUnique({
where: {
promptId_version: {
promptId: prompt.id,
version: Number(promptVersion),
},
},
});
if (!version) return undefined;
return {
slug: promptSlug,
version: version.version,
labels: promptLabels ?? "",
model: version.model ?? undefined,
template: version.textContent ?? undefined,
text: undefined,
input: promptInput,
};
}
async #getTaskRunContext({
run,
machine,
environment,
lockedWorker,
}: {
run: FindRunResult;
machine?: MachinePreset;
environment: AuthenticatedEnvironment;
lockedWorker: ResolvedRunLockedWorker | null;
}) {
if (run.engine === "V1") {
return this.#getV3TaskRunContext({ run, machine, environment, lockedWorker });
} else {
return this.#getV4TaskRunContext({ run });
}
}
async #getV3TaskRunContext({
run,
machine,
environment,
lockedWorker,
}: {
run: FindRunResult;
machine?: MachinePreset;
environment: AuthenticatedEnvironment;
lockedWorker: ResolvedRunLockedWorker | null;
}): Promise<V3TaskRunContext> {
const attempt = run.attempts[0];
const context = {
attempt: attempt
? {
id: attempt.friendlyId,
number: attempt.number,
status: attempt.status,
startedAt: attempt.createdAt,
}
: {
id: AttemptId.generate().friendlyId,
number: 1,
status: "PENDING" as const,
startedAt: run.updatedAt,
},
task: {
id: run.taskIdentifier,
filePath: lockedWorker?.lockedBy?.filePath ?? "",
},
run: {
id: run.friendlyId,
createdAt: run.createdAt,
tags: run.runTags,
isTest: run.isTest,
isReplay: !!run.replayedFromTaskRunFriendlyId,
idempotencyKey: getUserProvidedIdempotencyKey(run) ?? undefined,
startedAt: run.startedAt ?? run.createdAt,
durationMs: run.usageDurationMs,
costInCents: run.costInCents,
baseCostInCents: run.baseCostInCents,
maxAttempts: run.maxAttempts ?? undefined,
version: lockedWorker?.lockedToVersion?.version,
maxDuration: run.maxDurationInSeconds ?? undefined,
},
queue: {
name: run.queue,
id: run.queue,
},
environment: {
id: environment.id,
slug: environment.slug,
type: environment.type,
},
organization: {
id: environment.organization.id,
slug: environment.organization.slug,
name: environment.organization.title,
},
project: {
id: environment.project.id,
ref: environment.project.externalRef,
slug: environment.project.slug,
name: environment.project.name,
},
machine,
} satisfies V3TaskRunContext;
return context;
}
async #getV4TaskRunContext({ run }: { run: FindRunResult }): Promise<TaskRunContext> {
return engine.resolveTaskRunContext(run.id);
}
#getExternalTraceId(traceContext: unknown) {
if (!traceContext) {
return;
}
const parsedTraceContext = TriggerTraceContext.safeParse(traceContext);
if (!parsedTraceContext.success) {
return;
}
const externalTraceparent = parsedTraceContext.data.external?.traceparent;
if (!externalTraceparent) {
return;
}
const parsedTraceparent = parseTraceparent(externalTraceparent);
return parsedTraceparent?.traceId;
}
}