Skip to content

Commit 4b28080

Browse files
feat: add isReplay to run context (#3454)
## Summary Adds `isReplay` boolean to the run context (`ctx.run.isReplay`), following the same pattern as the existing `isTest`. The value is derived from the existing `replayedFromTaskRunFriendlyId` database field, so no schema migration is needed. ## ✅ Checklist - [x] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) - [x] The PR title follows the convention. - [x] I ran and tested the code works --- ## Testing - Verified `@trigger.dev/core` builds successfully - Verified `webapp` typechecks successfully - All new fields use `default(false)` for backwards compatibility --- ## Changelog - Added `isReplay` to `TaskRun` and `V3TaskRun` schemas in `common.ts` - Added `RUN_IS_REPLAY` semantic attribute and wired it in `taskContext` - Propagated `isReplay` through the dequeue system, run attempt system, and all execution context construction paths (V1 + V2) - Added `isReplay` to `DequeuedMessage` and `TaskRunExecutionLazyAttemptPayload` schemas - Added patch changeset for `@trigger.dev/core` - Updated docs: added `isReplay` to context reference, added "Detecting replays" section to replaying page --- 💯 Link to Devin session: https://app.devin.ai/sessions/1d6f1b3cc39a4623b72d05bf00f2d70c --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: nick <55853254+nicktrn@users.noreply.github.com>
1 parent 91fd8a8 commit 4b28080

14 files changed

Lines changed: 92 additions & 59 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/core": patch
3+
---
4+
5+
Add `isReplay` boolean to the run context (`ctx.run.isReplay`), derived from the existing `replayedFromTaskRunFriendlyId` database field. Defaults to `false` for backwards compatibility.

apps/webapp/app/presenters/v3/SpanPresenter.server.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ export type PromptSpanData = {
4242
config?: string;
4343
};
4444

45-
function extractPromptSpanData(
46-
properties: Record<string, unknown>
47-
): PromptSpanData | undefined {
45+
function extractPromptSpanData(properties: Record<string, unknown>): PromptSpanData | undefined {
4846
// Properties come as an unflattened nested object from ClickHouse,
4947
// e.g. { prompt: { slug: "...", version: 3, ... } }
5048
const prompt = properties.prompt;
@@ -592,10 +590,7 @@ export class SpanPresenter extends BasePresenter {
592590
triggeredRuns,
593591
aiData:
594592
span.properties && typeof span.properties === "object"
595-
? extractAISpanData(
596-
span.properties as Record<string, unknown>,
597-
span.duration / 1_000_000
598-
)
593+
? extractAISpanData(span.properties as Record<string, unknown>, span.duration / 1_000_000)
599594
: undefined,
600595
};
601596

@@ -739,10 +734,7 @@ export class SpanPresenter extends BasePresenter {
739734
"ai.streamObject",
740735
];
741736

742-
if (
743-
typeof span.message === "string" &&
744-
AI_SUMMARY_MESSAGES.includes(span.message)
745-
) {
737+
if (typeof span.message === "string" && AI_SUMMARY_MESSAGES.includes(span.message)) {
746738
const aiSummaryData = extractAISummarySpanData(
747739
span.properties as Record<string, unknown>,
748740
span.duration / 1_000_000
@@ -899,6 +891,7 @@ export class SpanPresenter extends BasePresenter {
899891
createdAt: run.createdAt,
900892
tags: run.runTags,
901893
isTest: run.isTest,
894+
isReplay: !!run.replayedFromTaskRunFriendlyId,
902895
idempotencyKey: getUserProvidedIdempotencyKey(run) ?? undefined,
903896
startedAt: run.startedAt ?? run.createdAt,
904897
durationMs: run.usageDurationMs,

apps/webapp/app/v3/marqs/devQueueConsumer.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,7 @@ export class DevQueueConsumer {
519519
runId: lockedTaskRun.friendlyId,
520520
messageId: lockedTaskRun.id,
521521
isTest: lockedTaskRun.isTest,
522+
isReplay: !!lockedTaskRun.replayedFromTaskRunFriendlyId,
522523
metrics: [
523524
{
524525
name: "start",

apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,7 @@ export const AttemptForExecutionGetPayload = {
16401640
createdAt: true,
16411641
startedAt: true,
16421642
isTest: true,
1643+
replayedFromTaskRunFriendlyId: true,
16431644
metadata: true,
16441645
metadataType: true,
16451646
idempotencyKey: true,
@@ -1726,6 +1727,7 @@ class SharedQueueTasks {
17261727
startedAt: taskRun.startedAt ?? taskRun.createdAt,
17271728
tags: taskRun.runTags ?? [],
17281729
isTest: taskRun.isTest,
1730+
isReplay: !!taskRun.replayedFromTaskRunFriendlyId,
17291731
idempotencyKey: taskRun.idempotencyKey ?? undefined,
17301732
durationMs: taskRun.usageDurationMs,
17311733
costInCents: taskRun.costInCents,
@@ -2045,6 +2047,7 @@ class SharedQueueTasks {
20452047
traceContext: true,
20462048
friendlyId: true,
20472049
isTest: true,
2050+
replayedFromTaskRunFriendlyId: true,
20482051
lockedBy: {
20492052
select: {
20502053
machineConfig: true,
@@ -2090,6 +2093,7 @@ class SharedQueueTasks {
20902093
runId: run.friendlyId,
20912094
messageId: run.id,
20922095
isTest: run.isTest,
2096+
isReplay: !!run.replayedFromTaskRunFriendlyId,
20932097
attemptCount,
20942098
metrics: [],
20952099
} satisfies TaskRunExecutionLazyAttemptPayload;

apps/webapp/app/v3/services/createTaskRunAttempt.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ export class CreateTaskRunAttemptService extends BaseService {
210210
createdAt: taskRun.createdAt,
211211
tags: taskRun.runTags ?? [],
212212
isTest: taskRun.isTest,
213+
isReplay: !!taskRun.replayedFromTaskRunFriendlyId,
213214
idempotencyKey: taskRun.idempotencyKey ?? undefined,
214215
startedAt: taskRun.startedAt ?? taskRun.createdAt,
215216
durationMs: taskRun.usageDurationMs,

docs/context.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ export const parentTask = task({
8181
<ResponseField name="isTest" type="boolean">
8282
Whether this is a [test run](/run-tests).
8383
</ResponseField>
84+
<ResponseField name="isReplay" type="boolean">
85+
Whether this run is a [replay](/replaying) of a previous run.
86+
</ResponseField>
8487
<ResponseField name="createdAt" type="date">
8588
The creation time of the task run.
8689
</ResponseField>

docs/replaying.mdx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ description: "A replay is a copy of a run with the same payload but against the
3030
</Tab>
3131
</Tabs>
3232

33+
### Detecting replays in your task
34+
35+
You can check if a run is a replay using the [context](/context) object:
36+
37+
```ts
38+
export const myTask = task({
39+
id: "my-task",
40+
run: async (payload, { ctx }) => {
41+
if (ctx.run.isReplay) {
42+
// This run is a replay of a previous run
43+
}
44+
},
45+
});
46+
```
47+
3348
### Replaying using the SDK
3449

3550
You can replay a run using the SDK:

internal-packages/run-engine/src/engine/systems/dequeueSystem.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ export class DequeueSystem {
607607
id: lockedTaskRun.id,
608608
friendlyId: lockedTaskRun.friendlyId,
609609
isTest: lockedTaskRun.isTest,
610+
isReplay: !!lockedTaskRun.replayedFromTaskRunFriendlyId,
610611
machine: machinePreset,
611612
attemptNumber: nextAttemptNumber,
612613
// Keeping this for backwards compatibility, but really this should be called workerQueue

internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ export class RunAttemptSystem {
196196
machinePreset: true,
197197
runTags: true,
198198
isTest: true,
199+
replayedFromTaskRunFriendlyId: true,
199200
idempotencyKey: true,
200201
idempotencyKeyOptions: true,
201202
startedAt: true,
@@ -232,9 +233,9 @@ export class RunAttemptSystem {
232233
run.lockedById
233234
? this.#resolveTaskRunExecutionTask(run.lockedById)
234235
: Promise.resolve({
235-
id: run.taskIdentifier,
236-
filePath: "unknown",
237-
}),
236+
id: run.taskIdentifier,
237+
filePath: "unknown",
238+
}),
238239
this.#resolveTaskRunExecutionQueue({
239240
lockedQueueId: run.lockedQueueId ?? undefined,
240241
queueName: run.queue,
@@ -245,13 +246,13 @@ export class RunAttemptSystem {
245246
run.lockedById
246247
? this.#resolveTaskRunExecutionMachinePreset(run.lockedById, run.machinePreset)
247248
: Promise.resolve(
248-
getMachinePreset({
249-
defaultMachine: this.options.machines.defaultMachine,
250-
machines: this.options.machines.machines,
251-
config: undefined,
252-
run,
253-
})
254-
),
249+
getMachinePreset({
250+
defaultMachine: this.options.machines.defaultMachine,
251+
machines: this.options.machines.machines,
252+
config: undefined,
253+
run,
254+
})
255+
),
255256
run.lockedById
256257
? this.#resolveTaskRunExecutionDeployment(run.lockedById)
257258
: Promise.resolve(undefined),
@@ -262,6 +263,7 @@ export class RunAttemptSystem {
262263
id: run.friendlyId,
263264
tags: run.runTags,
264265
isTest: run.isTest,
266+
isReplay: !!run.replayedFromTaskRunFriendlyId,
265267
createdAt: run.createdAt,
266268
startedAt: run.startedAt ?? run.createdAt,
267269
idempotencyKey: getUserProvidedIdempotencyKey(run) ?? undefined,
@@ -426,6 +428,7 @@ export class RunAttemptSystem {
426428
payloadType: true,
427429
runTags: true,
428430
isTest: true,
431+
replayedFromTaskRunFriendlyId: true,
429432
idempotencyKey: true,
430433
idempotencyKeyOptions: true,
431434
startedAt: true,
@@ -459,8 +462,9 @@ export class RunAttemptSystem {
459462
run,
460463
snapshot: {
461464
executionStatus: "EXECUTING",
462-
description: `Attempt created, starting execution${isWarmStart ? " (warm start)" : ""
463-
}`,
465+
description: `Attempt created, starting execution${
466+
isWarmStart ? " (warm start)" : ""
467+
}`,
464468
},
465469
previousSnapshotId: latestSnapshot.id,
466470
environmentId: latestSnapshot.environmentId,
@@ -574,6 +578,7 @@ export class RunAttemptSystem {
574578
createdAt: updatedRun.createdAt,
575579
tags: updatedRun.runTags,
576580
isTest: updatedRun.isTest,
581+
isReplay: !!updatedRun.replayedFromTaskRunFriendlyId,
577582
idempotencyKey: getUserProvidedIdempotencyKey(updatedRun) ?? undefined,
578583
idempotencyKeyScope: extractIdempotencyKeyScope(updatedRun),
579584
startedAt: updatedRun.startedAt ?? updatedRun.createdAt,
@@ -618,8 +623,8 @@ export class RunAttemptSystem {
618623
deployment,
619624
batch: updatedRun.batchId
620625
? {
621-
id: BatchId.toFriendlyId(updatedRun.batchId),
622-
}
626+
id: BatchId.toFriendlyId(updatedRun.batchId),
627+
}
623628
: undefined,
624629
};
625630

@@ -1387,8 +1392,8 @@ export class RunAttemptSystem {
13871392
error,
13881393
bulkActionGroupIds: bulkActionId
13891394
? {
1390-
push: bulkActionId,
1391-
}
1395+
push: bulkActionId,
1396+
}
13921397
: undefined,
13931398
...(usageUpdate && {
13941399
usageDurationMs: usageUpdate.usageDurationMs,
@@ -1876,26 +1881,26 @@ export class RunAttemptSystem {
18761881
const result = await this.cache.queues.swr(cacheKey, async () => {
18771882
const queue = params.lockedQueueId
18781883
? await this.$.readOnlyPrisma.taskQueue.findFirst({
1879-
where: {
1880-
id: params.lockedQueueId,
1881-
},
1882-
select: {
1883-
id: true,
1884-
friendlyId: true,
1885-
name: true,
1886-
},
1887-
})
1884+
where: {
1885+
id: params.lockedQueueId,
1886+
},
1887+
select: {
1888+
id: true,
1889+
friendlyId: true,
1890+
name: true,
1891+
},
1892+
})
18881893
: await this.$.readOnlyPrisma.taskQueue.findFirst({
1889-
where: {
1890-
runtimeEnvironmentId: params.runtimeEnvironmentId,
1891-
name: params.queueName,
1892-
},
1893-
select: {
1894-
id: true,
1895-
friendlyId: true,
1896-
name: true,
1897-
},
1898-
});
1894+
where: {
1895+
runtimeEnvironmentId: params.runtimeEnvironmentId,
1896+
name: params.queueName,
1897+
},
1898+
select: {
1899+
id: true,
1900+
friendlyId: true,
1901+
name: true,
1902+
},
1903+
});
18991904

19001905
if (!queue) {
19011906
// Return synthetic queue so run/span view still loads (e.g. createFailedTaskRun with fallback queue)
@@ -2068,13 +2073,13 @@ export class RunAttemptSystem {
20682073
if (environmentType !== "DEVELOPMENT") {
20692074
const machinePreset = machinePresetName
20702075
? machinePresetFromName(
2071-
this.options.machines.machines,
2072-
machinePresetName as MachinePresetName
2073-
)
2076+
this.options.machines.machines,
2077+
machinePresetName as MachinePresetName
2078+
)
20742079
: machinePresetFromName(
2075-
this.options.machines.machines,
2076-
this.options.machines.defaultMachine
2077-
);
2080+
this.options.machines.machines,
2081+
this.options.machines.defaultMachine
2082+
);
20782083

20792084
costInCents = currentCostInCents + attemptDurationMs * machinePreset.centsPerMs;
20802085
}
@@ -2084,7 +2089,6 @@ export class RunAttemptSystem {
20842089
costInCents,
20852090
};
20862091
}
2087-
20882092
}
20892093

20902094
export function safeParseGitMeta(git: unknown): GitMeta | undefined {

packages/core/src/v3/schemas/common.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export const TaskRun = z.object({
215215
payloadType: z.string(),
216216
tags: z.array(z.string()),
217217
isTest: z.boolean().default(false),
218+
isReplay: z.boolean().default(false),
218219
createdAt: z.coerce.date(),
219220
startedAt: z.coerce.date().default(() => new Date()),
220221
/** The user-provided idempotency key (not the hash) */
@@ -378,6 +379,7 @@ export const V3TaskRun = z.object({
378379
payloadType: z.string(),
379380
tags: z.array(z.string()),
380381
isTest: z.boolean().default(false),
382+
isReplay: z.boolean().default(false),
381383
createdAt: z.coerce.date(),
382384
startedAt: z.coerce.date().default(() => new Date()),
383385
/** The user-provided idempotency key (not the hash) */
@@ -538,13 +540,13 @@ export type WaitpointTokenResult = z.infer<typeof WaitpointTokenResult>;
538540

539541
export type WaitpointTokenTypedResult<T> =
540542
| {
541-
ok: true;
542-
output: T;
543-
}
543+
ok: true;
544+
output: T;
545+
}
544546
| {
545-
ok: false;
546-
error: Error;
547-
};
547+
ok: false;
548+
error: Error;
549+
};
548550

549551
export const SerializedError = z.object({
550552
message: z.string(),

0 commit comments

Comments
 (0)