From b384dad994a6bb121ed579cd7bf5764254168063 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:53:09 +0100 Subject: [PATCH 01/62] remove dead code --- .../cli-v3/src/entryPoints/dev-run-worker.ts | 9 ---- .../src/entryPoints/managed-run-worker.ts | 9 ---- .../cli-v3/src/executions/taskRunProcess.ts | 50 ------------------- .../src/v3/runtime/managedRuntimeManager.ts | 4 -- packages/core/src/v3/schemas/messages.ts | 29 ----------- 5 files changed, 101 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index fed66e6dc6..3d20547be5 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -452,18 +452,9 @@ const zodIpc = new ZodIpcConnection({ }); } }, - TASK_RUN_COMPLETED_NOTIFICATION: async () => { - await managedWorkerRuntime.completeWaitpoints([]); - }, - WAIT_COMPLETED_NOTIFICATION: async () => { - await managedWorkerRuntime.completeWaitpoints([]); - }, FLUSH: async ({ timeoutInMs }, sender) => { await flushAll(timeoutInMs); }, - WAITPOINT_CREATED: async ({ wait, waitpoint }) => { - managedWorkerRuntime.associateWaitWithWaitpoint(wait.id, waitpoint.id); - }, WAITPOINT_COMPLETED: async ({ waitpoint }) => { managedWorkerRuntime.completeWaitpoints([waitpoint]); }, diff --git a/packages/cli-v3/src/entryPoints/managed-run-worker.ts b/packages/cli-v3/src/entryPoints/managed-run-worker.ts index 19c8718cce..298695dd51 100644 --- a/packages/cli-v3/src/entryPoints/managed-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-run-worker.ts @@ -445,18 +445,9 @@ const zodIpc = new ZodIpcConnection({ }); } }, - TASK_RUN_COMPLETED_NOTIFICATION: async () => { - await managedWorkerRuntime.completeWaitpoints([]); - }, - WAIT_COMPLETED_NOTIFICATION: async () => { - await managedWorkerRuntime.completeWaitpoints([]); - }, FLUSH: async ({ timeoutInMs }, sender) => { await flushAll(timeoutInMs); }, - WAITPOINT_CREATED: async ({ wait, waitpoint }) => { - managedWorkerRuntime.associateWaitWithWaitpoint(wait.id, waitpoint.id); - }, WAITPOINT_COMPLETED: async ({ waitpoint }) => { managedWorkerRuntime.completeWaitpoints([waitpoint]); }, diff --git a/packages/cli-v3/src/executions/taskRunProcess.ts b/packages/cli-v3/src/executions/taskRunProcess.ts index ae95ecb109..c82a185982 100644 --- a/packages/cli-v3/src/executions/taskRunProcess.ts +++ b/packages/cli-v3/src/executions/taskRunProcess.ts @@ -278,56 +278,6 @@ export class TaskRunProcess { return result; } - taskRunCompletedNotification(completion: TaskRunExecutionResult) { - if (!completion.ok && typeof completion.retry !== "undefined") { - logger.debug( - "Task run completed with error and wants to retry, won't send task run completed notification" - ); - return; - } - - if (!this._child?.connected || this._isBeingKilled || this._child.killed) { - logger.debug( - "Child process not connected or being killed, can't send task run completed notification" - ); - return; - } - - this._ipc?.send("TASK_RUN_COMPLETED_NOTIFICATION", { - version: "v2", - completion, - }); - } - - waitCompletedNotification() { - if (!this._child?.connected || this._isBeingKilled || this._child.killed) { - console.error( - "Child process not connected or being killed, can't send wait completed notification" - ); - return; - } - - this._ipc?.send("WAIT_COMPLETED_NOTIFICATION", {}); - } - - waitpointCreated(waitId: string, waitpointId: string) { - if (!this._child?.connected || this._isBeingKilled || this._child.killed) { - console.error( - "Child process not connected or being killed, can't send waitpoint created notification" - ); - return; - } - - this._ipc?.send("WAITPOINT_CREATED", { - wait: { - id: waitId, - }, - waitpoint: { - id: waitpointId, - }, - }); - } - waitpointCompleted(waitpoint: CompletedWaitpoint) { if (!this._child?.connected || this._isBeingKilled || this._child.killed) { console.error( diff --git a/packages/core/src/v3/runtime/managedRuntimeManager.ts b/packages/core/src/v3/runtime/managedRuntimeManager.ts index d23f800d76..eccd97cf17 100644 --- a/packages/core/src/v3/runtime/managedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/managedRuntimeManager.ts @@ -146,10 +146,6 @@ export class ManagedRuntimeManager implements RuntimeManager { }); } - associateWaitWithWaitpoint(waitId: string, waitpointId: string) { - this.resolversByWaitpoint.set(waitpointId, waitId); - } - async completeWaitpoints(waitpoints: CompletedWaitpoint[]): Promise { await Promise.all(waitpoints.map((waitpoint) => this.completeWaitpoint(waitpoint))); } diff --git a/packages/core/src/v3/schemas/messages.ts b/packages/core/src/v3/schemas/messages.ts index edbdfac3de..cffcb7c15b 100644 --- a/packages/core/src/v3/schemas/messages.ts +++ b/packages/core/src/v3/schemas/messages.ts @@ -219,41 +219,12 @@ export const WorkerToExecutorMessageCatalog = { isWarmStart: z.boolean().optional(), }), }, - TASK_RUN_COMPLETED_NOTIFICATION: { - message: z.discriminatedUnion("version", [ - z.object({ - version: z.literal("v1"), - completion: TaskRunExecutionResult, - execution: TaskRunExecution, - }), - z.object({ - version: z.literal("v2"), - completion: TaskRunExecutionResult, - }), - ]), - }, - WAIT_COMPLETED_NOTIFICATION: { - message: z.object({ - version: z.literal("v1").default("v1"), - }), - }, FLUSH: { message: z.object({ timeoutInMs: z.number(), }), callback: z.void(), }, - WAITPOINT_CREATED: { - message: z.object({ - version: z.literal("v1").default("v1"), - wait: z.object({ - id: z.string(), - }), - waitpoint: z.object({ - id: z.string(), - }), - }), - }, WAITPOINT_COMPLETED: { message: z.object({ version: z.literal("v1").default("v1"), From 082926a5ed79bcd4b7504fc13adf07368c835311 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:28:28 +0100 Subject: [PATCH 02/62] rename managed to shared runtime manager --- packages/cli-v3/src/entryPoints/dev-run-worker.ts | 8 ++++---- packages/cli-v3/src/entryPoints/managed-run-worker.ts | 8 ++++---- .../{managedRuntimeManager.ts => sharedRuntimeManager.ts} | 4 ++-- packages/core/src/v3/workers/index.ts | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) rename packages/core/src/v3/runtime/{managedRuntimeManager.ts => sharedRuntimeManager.ts} (98%) diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index 3d20547be5..3baf555198 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -35,7 +35,7 @@ import { getEnvVar, getNumberEnvVar, logLevels, - ManagedRuntimeManager, + SharedRuntimeManager, OtelTaskLogger, populateEnv, StandardLifecycleHooksManager, @@ -456,7 +456,7 @@ const zodIpc = new ZodIpcConnection({ await flushAll(timeoutInMs); }, WAITPOINT_COMPLETED: async ({ waitpoint }) => { - managedWorkerRuntime.completeWaitpoints([waitpoint]); + sharedWorkerRuntime.completeWaitpoints([waitpoint]); }, }, }); @@ -528,8 +528,8 @@ async function flushMetadata(timeoutInMs: number = 10_000) { }; } -const managedWorkerRuntime = new ManagedRuntimeManager(zodIpc, showInternalLogs); -runtime.setGlobalRuntimeManager(managedWorkerRuntime); +const sharedWorkerRuntime = new SharedRuntimeManager(zodIpc, showInternalLogs); +runtime.setGlobalRuntimeManager(sharedWorkerRuntime); process.title = "trigger-managed-worker"; diff --git a/packages/cli-v3/src/entryPoints/managed-run-worker.ts b/packages/cli-v3/src/entryPoints/managed-run-worker.ts index 298695dd51..5d192e81b2 100644 --- a/packages/cli-v3/src/entryPoints/managed-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-run-worker.ts @@ -34,7 +34,7 @@ import { getEnvVar, getNumberEnvVar, logLevels, - ManagedRuntimeManager, + SharedRuntimeManager, OtelTaskLogger, populateEnv, ProdUsageManager, @@ -449,7 +449,7 @@ const zodIpc = new ZodIpcConnection({ await flushAll(timeoutInMs); }, WAITPOINT_COMPLETED: async ({ waitpoint }) => { - managedWorkerRuntime.completeWaitpoints([waitpoint]); + sharedWorkerRuntime.completeWaitpoints([waitpoint]); }, }, }); @@ -556,9 +556,9 @@ function initializeUsageManager({ timeout.setGlobalManager(new UsageTimeoutManager(devUsageManager)); } -const managedWorkerRuntime = new ManagedRuntimeManager(zodIpc, true); +const sharedWorkerRuntime = new SharedRuntimeManager(zodIpc, true); -runtime.setGlobalRuntimeManager(managedWorkerRuntime); +runtime.setGlobalRuntimeManager(sharedWorkerRuntime); process.title = "trigger-managed-worker"; diff --git a/packages/core/src/v3/runtime/managedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts similarity index 98% rename from packages/core/src/v3/runtime/managedRuntimeManager.ts rename to packages/core/src/v3/runtime/sharedRuntimeManager.ts index eccd97cf17..855866e123 100644 --- a/packages/core/src/v3/runtime/managedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -15,7 +15,7 @@ import { preventMultipleWaits } from "./preventMultipleWaits.js"; type Resolver = (value: CompletedWaitpoint) => void; -export class ManagedRuntimeManager implements RuntimeManager { +export class SharedRuntimeManager implements RuntimeManager { // Maps a resolver ID to a resolver function private readonly resolversByWaitId: Map = new Map(); // Maps a waitpoint ID to a wait ID @@ -29,7 +29,7 @@ export class ManagedRuntimeManager implements RuntimeManager { ) { // Log out the runtime status on a long interval to help debug stuck executions setInterval(() => { - this.log("[DEBUG] ManagedRuntimeManager status", this.status); + this.log("[DEBUG] SharedRuntimeManager status", this.status); }, 300_000); } diff --git a/packages/core/src/v3/workers/index.ts b/packages/core/src/v3/workers/index.ts index de89b5587b..5ff626f2ea 100644 --- a/packages/core/src/v3/workers/index.ts +++ b/packages/core/src/v3/workers/index.ts @@ -21,7 +21,7 @@ export { ProdUsageManager, type ProdUsageManagerOptions } from "../usage/prodUsa export { UsageTimeoutManager } from "../timeout/usageTimeoutManager.js"; export { StandardMetadataManager } from "../runMetadata/manager.js"; export { StandardWaitUntilManager } from "../waitUntil/manager.js"; -export { ManagedRuntimeManager } from "../runtime/managedRuntimeManager.js"; +export { SharedRuntimeManager } from "../runtime/sharedRuntimeManager.js"; export * from "../runEngineWorker/index.js"; export { StandardRunTimelineMetricsManager } from "../runTimelineMetrics/runTimelineMetricsManager.js"; export { WarmStartClient, type WarmStartClientOptions } from "../workers/warmStartClient.js"; From 786416ccf91dd38e9bc2a3e6de5aaa22efb69ae8 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:09:21 +0100 Subject: [PATCH 03/62] rename to resolve waitpoint for clarity --- packages/cli-v3/src/entryPoints/dev-run-worker.ts | 4 ++-- packages/cli-v3/src/entryPoints/managed-run-worker.ts | 4 ++-- packages/core/src/v3/runtime/sharedRuntimeManager.ts | 8 ++++---- packages/core/src/v3/schemas/messages.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index 3baf555198..e88107b305 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -455,8 +455,8 @@ const zodIpc = new ZodIpcConnection({ FLUSH: async ({ timeoutInMs }, sender) => { await flushAll(timeoutInMs); }, - WAITPOINT_COMPLETED: async ({ waitpoint }) => { - sharedWorkerRuntime.completeWaitpoints([waitpoint]); + RESOLVE_WAITPOINT: async ({ waitpoint }) => { + sharedWorkerRuntime.resolveWaitpoints([waitpoint]); }, }, }); diff --git a/packages/cli-v3/src/entryPoints/managed-run-worker.ts b/packages/cli-v3/src/entryPoints/managed-run-worker.ts index 5d192e81b2..2553573a16 100644 --- a/packages/cli-v3/src/entryPoints/managed-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-run-worker.ts @@ -448,8 +448,8 @@ const zodIpc = new ZodIpcConnection({ FLUSH: async ({ timeoutInMs }, sender) => { await flushAll(timeoutInMs); }, - WAITPOINT_COMPLETED: async ({ waitpoint }) => { - sharedWorkerRuntime.completeWaitpoints([waitpoint]); + RESOLVE_WAITPOINT: async ({ waitpoint }) => { + sharedWorkerRuntime.resolveWaitpoints([waitpoint]); }, }, }); diff --git a/packages/core/src/v3/runtime/sharedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts index 855866e123..f282142828 100644 --- a/packages/core/src/v3/runtime/sharedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -146,12 +146,12 @@ export class SharedRuntimeManager implements RuntimeManager { }); } - async completeWaitpoints(waitpoints: CompletedWaitpoint[]): Promise { - await Promise.all(waitpoints.map((waitpoint) => this.completeWaitpoint(waitpoint))); + async resolveWaitpoints(waitpoints: CompletedWaitpoint[]): Promise { + await Promise.all(waitpoints.map((waitpoint) => this.resolveWaitpoint(waitpoint))); } - private completeWaitpoint(waitpoint: CompletedWaitpoint): void { - this.log("completeWaitpoint", waitpoint); + private resolveWaitpoint(waitpoint: CompletedWaitpoint): void { + this.log("resolveWaitpoint", waitpoint); let waitId: string | undefined; diff --git a/packages/core/src/v3/schemas/messages.ts b/packages/core/src/v3/schemas/messages.ts index cffcb7c15b..47e6cfadfd 100644 --- a/packages/core/src/v3/schemas/messages.ts +++ b/packages/core/src/v3/schemas/messages.ts @@ -225,7 +225,7 @@ export const WorkerToExecutorMessageCatalog = { }), callback: z.void(), }, - WAITPOINT_COMPLETED: { + RESOLVE_WAITPOINT: { message: z.object({ version: z.literal("v1").default("v1"), waitpoint: CompletedWaitpoint, From b5ae5586898077717fd1eb06707b48602992de93 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:10:29 +0100 Subject: [PATCH 04/62] add resolver id helper --- .../src/v3/runtime/sharedRuntimeManager.ts | 93 ++++++++++++------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/packages/core/src/v3/runtime/sharedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts index f282142828..05add48a6e 100644 --- a/packages/core/src/v3/runtime/sharedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -1,3 +1,4 @@ +import { assertExhaustive } from "../../utils.js"; import { clock } from "../clock-api.js"; import { lifecycleHooks } from "../lifecycle-hooks-api.js"; import { @@ -17,9 +18,7 @@ type Resolver = (value: CompletedWaitpoint) => void; export class SharedRuntimeManager implements RuntimeManager { // Maps a resolver ID to a resolver function - private readonly resolversByWaitId: Map = new Map(); - // Maps a waitpoint ID to a wait ID - private readonly resolversByWaitpoint: Map = new Map(); + private readonly resolversById: Map = new Map(); private _preventMultipleWaits = preventMultipleWaits(); @@ -40,7 +39,7 @@ export class SharedRuntimeManager implements RuntimeManager { async waitForTask(params: { id: string; ctx: TaskRunContext }): Promise { return this._preventMultipleWaits(async () => { const promise = new Promise((resolve) => { - this.resolversByWaitId.set(params.id, resolve); + this.resolversById.set(params.id, resolve); }); await lifecycleHooks.callOnWaitHookListeners({ @@ -70,14 +69,13 @@ export class SharedRuntimeManager implements RuntimeManager { return Promise.resolve({ id: params.id, items: [] }); } - const promise = Promise.all( - Array.from({ length: params.runCount }, (_, index) => { - const resolverId = `${params.id}_${index}`; - return new Promise((resolve, reject) => { - this.resolversByWaitId.set(resolverId, resolve); - }); - }) - ); + const promises = Array.from({ length: params.runCount }, (_, index) => { + const resolverId = `${params.id}_${index}`; + + return new Promise((resolve, reject) => { + this.resolversById.set(resolverId, resolve); + }); + }); await lifecycleHooks.callOnWaitHookListeners({ type: "batch", @@ -85,7 +83,7 @@ export class SharedRuntimeManager implements RuntimeManager { runCount: params.runCount, }); - const waitpoints = await promise; + const waitpoints = await Promise.all(promises); await lifecycleHooks.callOnResumeHookListeners({ type: "batch", @@ -109,7 +107,7 @@ export class SharedRuntimeManager implements RuntimeManager { }): Promise { return this._preventMultipleWaits(async () => { const promise = new Promise((resolve) => { - this.resolversByWaitId.set(waitpointFriendlyId, resolve); + this.resolversById.set(waitpointFriendlyId, resolve); }); if (finishDate) { @@ -150,36 +148,60 @@ export class SharedRuntimeManager implements RuntimeManager { await Promise.all(waitpoints.map((waitpoint) => this.resolveWaitpoint(waitpoint))); } + private resolverIdFromWaitpoint(waitpoint: CompletedWaitpoint): string | null { + switch (waitpoint.type) { + case "RUN": { + if (!waitpoint.completedByTaskRun) { + this.log("No completedByTaskRun for RUN waitpoint", waitpoint); + return null; + } + + if (waitpoint.completedByTaskRun.batch) { + // This run is part of a batch + return `${waitpoint.completedByTaskRun.batch.friendlyId}_${waitpoint.index}`; + } else { + // This run is NOT part of a batch + return waitpoint.completedByTaskRun.friendlyId; + } + } + case "BATCH": { + if (!waitpoint.completedByBatch) { + this.log("No completedByBatch for BATCH waitpoint", waitpoint); + return null; + } + + return waitpoint.completedByBatch.friendlyId; + } + case "MANUAL": + case "DATETIME": { + return waitpoint.friendlyId; + } + default: { + assertExhaustive(waitpoint.type); + } + } + } + private resolveWaitpoint(waitpoint: CompletedWaitpoint): void { this.log("resolveWaitpoint", waitpoint); - let waitId: string | undefined; - - if (waitpoint.completedByTaskRun) { - if (waitpoint.completedByTaskRun.batch) { - waitId = `${waitpoint.completedByTaskRun.batch.friendlyId}_${waitpoint.index}`; - } else { - waitId = waitpoint.completedByTaskRun.friendlyId; - } - } else if (waitpoint.completedByBatch) { - //no waitpoint resolves associated with batch completions - //a batch completion isn't when all the runs from a batch are completed + if (waitpoint.type === "BATCH") { + // We currently ignore these, they're not required to resume after a batch completes + this.log("Ignoring BATCH waitpoint", waitpoint); return; - } else if (waitpoint.type === "MANUAL" || waitpoint.type === "DATETIME") { - waitId = waitpoint.friendlyId; - } else { - waitId = this.resolversByWaitpoint.get(waitpoint.id); } - if (!waitId) { - this.log("No waitId found for waitpoint", { ...this.status, ...waitpoint }); + const resolverId = this.resolverIdFromWaitpoint(waitpoint); + + if (!resolverId) { + this.log("No resolverId found for waitpoint", { ...this.status, ...waitpoint }); return; } - const resolve = this.resolversByWaitId.get(waitId); + const resolve = this.resolversById.get(resolverId); if (!resolve) { - this.log("No resolver found for waitId", { ...this.status, waitId }); + this.log("No resolver found for resolverId", { ...this.status, resolverId }); return; } @@ -190,7 +212,7 @@ export class SharedRuntimeManager implements RuntimeManager { resolve(waitpoint); - this.resolversByWaitId.delete(waitId); + this.resolversById.delete(resolverId); } private waitpointToTaskRunExecutionResult(waitpoint: CompletedWaitpoint): TaskRunExecutionResult { @@ -224,8 +246,7 @@ export class SharedRuntimeManager implements RuntimeManager { private get status() { return { - resolversbyWaitId: Array.from(this.resolversByWaitId.keys()), - resolversByWaitpoint: Array.from(this.resolversByWaitpoint.keys()), + resolversById: Array.from(this.resolversById.keys()), }; } } From 4d46d20248cda9c0ce610c13a6e6ee5f8bc5011f Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:40:06 +0100 Subject: [PATCH 05/62] store and correctly resolve waipoints that come in early --- .../src/v3/runtime/sharedRuntimeManager.ts | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/core/src/v3/runtime/sharedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts index 05add48a6e..112d7de614 100644 --- a/packages/core/src/v3/runtime/sharedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -17,8 +17,11 @@ import { preventMultipleWaits } from "./preventMultipleWaits.js"; type Resolver = (value: CompletedWaitpoint) => void; export class SharedRuntimeManager implements RuntimeManager { - // Maps a resolver ID to a resolver function - private readonly resolversById: Map = new Map(); + /** Maps a resolver ID to a resolver function */ + private readonly resolversById = new Map(); + + /** Stores waitpoints that arrive before their resolvers have been created */ + private readonly waitpointsByResolverId = new Map(); private _preventMultipleWaits = preventMultipleWaits(); @@ -42,6 +45,9 @@ export class SharedRuntimeManager implements RuntimeManager { this.resolversById.set(params.id, resolve); }); + // Resolve any waitpoints we received before the resolver was created + this.resolvePendingWaitpoints(); + await lifecycleHooks.callOnWaitHookListeners({ type: "task", runId: params.id, @@ -77,6 +83,9 @@ export class SharedRuntimeManager implements RuntimeManager { }); }); + // Resolve any waitpoints we received before the resolvers were created + this.resolvePendingWaitpoints(); + await lifecycleHooks.callOnWaitHookListeners({ type: "batch", batchId: params.id, @@ -110,6 +119,9 @@ export class SharedRuntimeManager implements RuntimeManager { this.resolversById.set(waitpointFriendlyId, resolve); }); + // Resolve any waitpoints we received before the resolver was created + this.resolvePendingWaitpoints(); + if (finishDate) { await lifecycleHooks.callOnWaitHookListeners({ type: "duration", @@ -182,7 +194,7 @@ export class SharedRuntimeManager implements RuntimeManager { } } - private resolveWaitpoint(waitpoint: CompletedWaitpoint): void { + private resolveWaitpoint(waitpoint: CompletedWaitpoint, resolverId?: string | null): void { this.log("resolveWaitpoint", waitpoint); if (waitpoint.type === "BATCH") { @@ -191,10 +203,12 @@ export class SharedRuntimeManager implements RuntimeManager { return; } - const resolverId = this.resolverIdFromWaitpoint(waitpoint); + resolverId = resolverId ?? this.resolverIdFromWaitpoint(waitpoint); if (!resolverId) { - this.log("No resolverId found for waitpoint", { ...this.status, ...waitpoint }); + this.log("No resolverId for waitpoint", { ...this.status, ...waitpoint }); + + // No need to store the waitpoint, we'll never be able to resolve it return; } @@ -202,17 +216,26 @@ export class SharedRuntimeManager implements RuntimeManager { if (!resolve) { this.log("No resolver found for resolverId", { ...this.status, resolverId }); + + // Store the waitpoint for later if we can't find a resolver + this.waitpointsByResolverId.set(resolverId, waitpoint); + return; } - this.log("Resolving waitpoint", waitpoint); - // Ensure current time is accurate before resolving the waitpoint clock.reset(); resolve(waitpoint); this.resolversById.delete(resolverId); + this.waitpointsByResolverId.delete(resolverId); + } + + private resolvePendingWaitpoints(): void { + for (const [resolverId, waitpoint] of this.waitpointsByResolverId.entries()) { + this.resolveWaitpoint(waitpoint, resolverId); + } } private waitpointToTaskRunExecutionResult(waitpoint: CompletedWaitpoint): TaskRunExecutionResult { @@ -247,6 +270,7 @@ export class SharedRuntimeManager implements RuntimeManager { private get status() { return { resolversById: Array.from(this.resolversById.keys()), + waitpointsByResolverId: Array.from(this.waitpointsByResolverId.keys()), }; } } From e72bcc8a1db32c6c0df1a4011ea61eacc9a2312d Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:44:01 +0100 Subject: [PATCH 06/62] fix ipc message type change --- packages/cli-v3/src/executions/taskRunProcess.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/cli-v3/src/executions/taskRunProcess.ts b/packages/cli-v3/src/executions/taskRunProcess.ts index c82a185982..f6ccbb16fa 100644 --- a/packages/cli-v3/src/executions/taskRunProcess.ts +++ b/packages/cli-v3/src/executions/taskRunProcess.ts @@ -286,9 +286,7 @@ export class TaskRunProcess { return; } - this._ipc?.send("WAITPOINT_COMPLETED", { - waitpoint, - }); + this._ipc?.send("RESOLVE_WAITPOINT", { waitpoint }); } async #handleExit(code: number | null, signal: NodeJS.Signals | null) { From 38273ae5c854f190250313b39087c1cb0de30a5c Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:15:08 +0100 Subject: [PATCH 07/62] branded type for resolver ids --- .../src/v3/runtime/sharedRuntimeManager.ts | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/core/src/v3/runtime/sharedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts index 112d7de614..9882e893b6 100644 --- a/packages/core/src/v3/runtime/sharedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -14,14 +14,18 @@ import { ExecutorToWorkerProcessConnection } from "../zodIpc.js"; import { RuntimeManager } from "./manager.js"; import { preventMultipleWaits } from "./preventMultipleWaits.js"; +/** A function that resolves a waitpoint */ type Resolver = (value: CompletedWaitpoint) => void; +/** Branded type for resolver IDs to keep us from doing anything stupid */ +type ResolverId = string & { readonly __brand: unique symbol }; + export class SharedRuntimeManager implements RuntimeManager { /** Maps a resolver ID to a resolver function */ - private readonly resolversById = new Map(); + private readonly resolversById = new Map(); /** Stores waitpoints that arrive before their resolvers have been created */ - private readonly waitpointsByResolverId = new Map(); + private readonly waitpointsByResolverId = new Map(); private _preventMultipleWaits = preventMultipleWaits(); @@ -42,7 +46,7 @@ export class SharedRuntimeManager implements RuntimeManager { async waitForTask(params: { id: string; ctx: TaskRunContext }): Promise { return this._preventMultipleWaits(async () => { const promise = new Promise((resolve) => { - this.resolversById.set(params.id, resolve); + this.resolversById.set(params.id as ResolverId, resolve); }); // Resolve any waitpoints we received before the resolver was created @@ -76,7 +80,7 @@ export class SharedRuntimeManager implements RuntimeManager { } const promises = Array.from({ length: params.runCount }, (_, index) => { - const resolverId = `${params.id}_${index}`; + const resolverId = `${params.id}_${index}` as ResolverId; return new Promise((resolve, reject) => { this.resolversById.set(resolverId, resolve); @@ -116,7 +120,7 @@ export class SharedRuntimeManager implements RuntimeManager { }): Promise { return this._preventMultipleWaits(async () => { const promise = new Promise((resolve) => { - this.resolversById.set(waitpointFriendlyId, resolve); + this.resolversById.set(waitpointFriendlyId as ResolverId, resolve); }); // Resolve any waitpoints we received before the resolver was created @@ -160,7 +164,9 @@ export class SharedRuntimeManager implements RuntimeManager { await Promise.all(waitpoints.map((waitpoint) => this.resolveWaitpoint(waitpoint))); } - private resolverIdFromWaitpoint(waitpoint: CompletedWaitpoint): string | null { + private resolverIdFromWaitpoint(waitpoint: CompletedWaitpoint): ResolverId | null { + let id: string; + switch (waitpoint.type) { case "RUN": { if (!waitpoint.completedByTaskRun) { @@ -170,11 +176,13 @@ export class SharedRuntimeManager implements RuntimeManager { if (waitpoint.completedByTaskRun.batch) { // This run is part of a batch - return `${waitpoint.completedByTaskRun.batch.friendlyId}_${waitpoint.index}`; + id = `${waitpoint.completedByTaskRun.batch.friendlyId}_${waitpoint.index}`; } else { // This run is NOT part of a batch - return waitpoint.completedByTaskRun.friendlyId; + id = waitpoint.completedByTaskRun.friendlyId; } + + break; } case "BATCH": { if (!waitpoint.completedByBatch) { @@ -182,19 +190,23 @@ export class SharedRuntimeManager implements RuntimeManager { return null; } - return waitpoint.completedByBatch.friendlyId; + id = waitpoint.completedByBatch.friendlyId; + break; } case "MANUAL": case "DATETIME": { - return waitpoint.friendlyId; + id = waitpoint.friendlyId; + break; } default: { assertExhaustive(waitpoint.type); } } + + return id as ResolverId; } - private resolveWaitpoint(waitpoint: CompletedWaitpoint, resolverId?: string | null): void { + private resolveWaitpoint(waitpoint: CompletedWaitpoint, resolverId?: ResolverId | null): void { this.log("resolveWaitpoint", waitpoint); if (waitpoint.type === "BATCH") { From f190420855e58007f80581deffbdfff80c775182 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:16:52 +0100 Subject: [PATCH 08/62] add fixme comments --- packages/core/src/v3/runtime/sharedRuntimeManager.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/src/v3/runtime/sharedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts index 9882e893b6..2f38ec7f85 100644 --- a/packages/core/src/v3/runtime/sharedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -57,6 +57,8 @@ export class SharedRuntimeManager implements RuntimeManager { runId: params.id, }); + // FIXME: We need to send a "ready to checkpoint" signal here + const waitpoint = await promise; const result = this.waitpointToTaskRunExecutionResult(waitpoint); @@ -96,6 +98,8 @@ export class SharedRuntimeManager implements RuntimeManager { runCount: params.runCount, }); + // FIXME: We need to send a "ready to checkpoint" signal here + const waitpoints = await Promise.all(promises); await lifecycleHooks.callOnResumeHookListeners({ @@ -138,6 +142,8 @@ export class SharedRuntimeManager implements RuntimeManager { }); } + // FIXME: We need to send a "ready to checkpoint" signal here + const waitpoint = await promise; if (finishDate) { From 3592db2566637a7ab5bf4d727c14eadb74133759 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 16:47:01 +0100 Subject: [PATCH 09/62] remove more unused ipc schemas --- .../cli-v3/src/executions/taskRunProcess.ts | 30 ------------------ packages/core/src/v3/schemas/messages.ts | 31 ------------------- packages/core/src/v3/schemas/schemas.ts | 14 --------- 3 files changed, 75 deletions(-) diff --git a/packages/cli-v3/src/executions/taskRunProcess.ts b/packages/cli-v3/src/executions/taskRunProcess.ts index f6ccbb16fa..9e9e8283b9 100644 --- a/packages/cli-v3/src/executions/taskRunProcess.ts +++ b/packages/cli-v3/src/executions/taskRunProcess.ts @@ -33,20 +33,6 @@ import { SuspendedProcessError, } from "@trigger.dev/core/v3/errors"; -export type OnWaitForDurationMessage = InferSocketMessageSchema< - typeof ExecutorToWorkerMessageCatalog, - "WAIT_FOR_DURATION" ->; -export type OnWaitForTaskMessage = InferSocketMessageSchema< - typeof ExecutorToWorkerMessageCatalog, - "WAIT_FOR_TASK" ->; -export type OnWaitForBatchMessage = InferSocketMessageSchema< - typeof ExecutorToWorkerMessageCatalog, - "WAIT_FOR_BATCH" ->; -export type OnWaitMessage = InferSocketMessageSchema; - export type TaskRunProcessOptions = { workerManifest: WorkerManifest; serverWorker: ServerBackgroundWorker; @@ -82,11 +68,6 @@ export class TaskRunProcess { public onExit: Evt<{ code: number | null; signal: NodeJS.Signals | null; pid?: number }> = new Evt(); public onIsBeingKilled: Evt = new Evt(); - public onReadyToDispose: Evt = new Evt(); - - public onWaitForTask: Evt = new Evt(); - public onWaitForBatch: Evt = new Evt(); - public onWait: Evt = new Evt(); private _isPreparedForNextRun: boolean = false; private _isPreparedForNextAttempt: boolean = false; @@ -191,20 +172,9 @@ export class TaskRunProcess { resolver(result); }, - READY_TO_DISPOSE: async () => { - logger.debug(`task run process is ready to dispose`); - - this.onReadyToDispose.post(this); - }, TASK_HEARTBEAT: async (message) => { this.onTaskRunHeartbeat.post(message.id); }, - WAIT_FOR_TASK: async (message) => { - this.onWaitForTask.post(message); - }, - WAIT_FOR_BATCH: async (message) => { - this.onWaitForBatch.post(message); - }, UNCAUGHT_EXCEPTION: async (message) => { logger.debug("uncaught exception in task run process", { ...message }); }, diff --git a/packages/core/src/v3/schemas/messages.ts b/packages/core/src/v3/schemas/messages.ts index 47e6cfadfd..8076af621a 100644 --- a/packages/core/src/v3/schemas/messages.ts +++ b/packages/core/src/v3/schemas/messages.ts @@ -13,7 +13,6 @@ import { ProdTaskRunExecution, ProdTaskRunExecutionPayload, RunEngineVersionSchema, - RuntimeWait, TaskRunExecutionLazyAttemptPayload, TaskRunExecutionMetrics, WaitReason, @@ -172,39 +171,9 @@ export const ExecutorToWorkerMessageCatalog = { id: z.string(), }), }, - READY_TO_DISPOSE: { - message: z.undefined(), - }, - WAIT_FOR_DURATION: { - message: z.object({ - version: z.literal("v1").default("v1"), - ms: z.number(), - now: z.number(), - waitThresholdInMs: z.number(), - }), - }, - WAIT_FOR_TASK: { - message: z.object({ - version: z.literal("v1").default("v1"), - friendlyId: z.string(), - }), - }, - WAIT_FOR_BATCH: { - message: z.object({ - version: z.literal("v1").default("v1"), - batchFriendlyId: z.string(), - runFriendlyIds: z.string().array(), - }), - }, UNCAUGHT_EXCEPTION: { message: UncaughtExceptionMessage, }, - WAIT: { - message: z.object({ - version: z.literal("v1").default("v1"), - wait: RuntimeWait, - }), - }, }; export const WorkerToExecutorMessageCatalog = { diff --git a/packages/core/src/v3/schemas/schemas.ts b/packages/core/src/v3/schemas/schemas.ts index 302fcf5f37..40ab6116ad 100644 --- a/packages/core/src/v3/schemas/schemas.ts +++ b/packages/core/src/v3/schemas/schemas.ts @@ -275,20 +275,6 @@ export const TaskRunExecutionLazyAttemptPayload = z.object({ export type TaskRunExecutionLazyAttemptPayload = z.infer; -export const RuntimeWait = z.discriminatedUnion("type", [ - z.object({ - type: z.literal("DATETIME"), - id: z.string(), - date: z.coerce.date(), - }), - z.object({ - type: z.literal("MANUAL"), - id: z.string(), - }), -]); - -export type RuntimeWait = z.infer; - export const ManualCheckpointMetadata = z.object({ /** NOT a friendly ID */ attemptId: z.string(), From b13d8b70c2c5e3408465610cf47315cfc1df4e2c Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 21:09:48 +0100 Subject: [PATCH 10/62] fix entitlement validation when client doesn't exist --- apps/webapp/app/runEngine/validators/triggerTaskValidator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/runEngine/validators/triggerTaskValidator.ts b/apps/webapp/app/runEngine/validators/triggerTaskValidator.ts index cd903d7763..e63bdacfb5 100644 --- a/apps/webapp/app/runEngine/validators/triggerTaskValidator.ts +++ b/apps/webapp/app/runEngine/validators/triggerTaskValidator.ts @@ -46,7 +46,7 @@ export class DefaultTriggerTaskValidator implements TriggerTaskValidator { const result = await getEntitlement(environment.organizationId); - if (!result || result.hasAccess === false) { + if (result && result.hasAccess === false) { return { ok: false, error: new OutOfEntitlementError(), From 3cb44a157c3a5df6b9bed891352b3a72d67e89be Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 21:10:18 +0100 Subject: [PATCH 11/62] restore hello world reference workspace imports --- pnpm-lock.yaml | 343 +++++++++++++--------------- references/hello-world/package.json | 4 +- 2 files changed, 162 insertions(+), 185 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1251783db1..d8c8d014e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1925,12 +1925,12 @@ importers: references/hello-world: dependencies: '@trigger.dev/sdk': - specifier: 4.0.0-v4-beta.7 - version: 4.0.0-v4-beta.7(zod@3.23.8) + specifier: workspace:* + version: link:../../packages/trigger-sdk devDependencies: trigger.dev: - specifier: 4.0.0-v4-beta.7 - version: 4.0.0-v4-beta.7(typescript@5.5.4) + specifier: workspace:* + version: link:../../packages/cli-v3 references/init-shell: devDependencies: @@ -2775,6 +2775,7 @@ packages: /@arr/every@1.0.1: resolution: {integrity: sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==} engines: {node: '>=4'} + dev: false /@aws-crypto/crc32@3.0.0: resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} @@ -5527,6 +5528,7 @@ packages: /@bugsnag/cuid@3.1.1: resolution: {integrity: sha512-d2z4b0rEo3chI07FNN1Xds8v25CNeekecU6FC/2Fs9MxY2EipkZTThVcV2YinMn8dvRUlViKOyC50evoUxg8tw==} + dev: false /@bundled-es-modules/cookie@2.0.0: resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} @@ -5768,6 +5770,7 @@ packages: dependencies: picocolors: 1.1.1 sisteransi: 1.0.5 + dev: false /@clack/prompts@0.10.0: resolution: {integrity: sha512-H3rCl6CwW1NdQt9rE3n373t7o5cthPv7yUoxF2ytZvyvlJv89C5RYMJu83Hed8ODgys5vpBU0GKxIRG83jd8NQ==} @@ -5775,6 +5778,7 @@ packages: '@clack/core': 0.4.1 picocolors: 1.1.1 sisteransi: 1.0.5 + dev: false /@cloudflare/kv-asset-handler@0.3.4: resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} @@ -6001,6 +6005,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: false optional: true /@depot/cli-darwin-x64@0.0.1-cli.2.80.0: @@ -6009,6 +6014,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: false optional: true /@depot/cli-linux-arm64@0.0.1-cli.2.80.0: @@ -6017,6 +6023,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: false optional: true /@depot/cli-linux-arm@0.0.1-cli.2.80.0: @@ -6025,6 +6032,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: false optional: true /@depot/cli-linux-ia32@0.0.1-cli.2.80.0: @@ -6033,6 +6041,7 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: false optional: true /@depot/cli-linux-x64@0.0.1-cli.2.80.0: @@ -6041,6 +6050,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: false optional: true /@depot/cli-win32-arm64@0.0.1-cli.2.80.0: @@ -6049,6 +6059,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: false optional: true /@depot/cli-win32-ia32@0.0.1-cli.2.80.0: @@ -6057,6 +6068,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: false optional: true /@depot/cli-win32-x64@0.0.1-cli.2.80.0: @@ -6065,6 +6077,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: false optional: true /@depot/cli@0.0.1-cli.2.80.0: @@ -6082,6 +6095,7 @@ packages: '@depot/cli-win32-arm64': 0.0.1-cli.2.80.0 '@depot/cli-win32-ia32': 0.0.1-cli.2.80.0 '@depot/cli-win32-x64': 0.0.1-cli.2.80.0 + dev: false /@depot/sdk-node@1.0.0: resolution: {integrity: sha512-zq1xqesqGhp58Rh0bTXhsjoC8X+L+LwZABLlQh7Rm45770nGtAABa3GtU1n/776YTMI1gIAhzc++Jxae3H0J4w==} @@ -6143,6 +6157,7 @@ packages: resolution: {integrity: sha512-Ei9jN3pDoGzc+a/bGqnB5ajb52IvSv7/n2btuyzUlcOHIR2kM9fqtYTJXPwZYKLkGZlHWlpHgWyRtrinkP2nHg==} optionalDependencies: '@rollup/rollup-darwin-arm64': 4.36.0 + dev: false /@electric-sql/react@0.3.5(react@18.2.0): resolution: {integrity: sha512-qPrlF3BsRg5L8zAn1sLGzc3pkswfEHyQI3lNOu7Xllv1DBx85RvHR1zgGGPAUfC8iwyWupQu9pFPE63GdbeuhA==} @@ -8040,6 +8055,7 @@ packages: /@google-cloud/precise-date@4.0.0: resolution: {integrity: sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==} engines: {node: '>=14.0.0'} + dev: false /@graphile/logger@0.2.0: resolution: {integrity: sha512-jjcWBokl9eb1gVJ85QmoaQ73CQ52xAaOCF29ukRbYNl6lY+ts0ErTaDYOBlejcbUs2OpaiqYLO5uDhyLFzWw4w==} @@ -8703,6 +8719,7 @@ packages: /@jsonhero/path@1.0.21: resolution: {integrity: sha512-gVUDj/92acpVoJwsVJ/RuWOaHyG4oFzn898WNGQItLCTQ+hOaVlEaImhwE1WqOTf+l3dGOUkbSiVKlb3q1hd1Q==} + dev: false /@jspm/core@2.0.1: resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==} @@ -8867,6 +8884,7 @@ packages: zod-to-json-schema: 3.24.3(zod@3.23.8) transitivePeerDependencies: - supports-color + dev: false /@mrleebo/prisma-ast@0.7.0: resolution: {integrity: sha512-GTPkYf1meO2UXXIrz/SIDFWz+P4kXo2PTt36LYh/oNxV1PieYi7ZgenQk4IV0ut71Je3Z8ZoNZ8Tr7v2c1X1pg==} @@ -9402,6 +9420,7 @@ packages: '@opentelemetry/api': '>=1.0.0 <1.10.0' dependencies: '@opentelemetry/api': 1.9.0 + dev: false /@opentelemetry/core@1.22.0(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-0VoAlT6x+Xzik1v9goJ3pZ2ppi6+xd3aUfg4brfrLkDBHRIVjMP0eBHrKrhB+NKcDyMAg8fAbGL3Npg/F6AwWA==} @@ -9440,6 +9459,7 @@ packages: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.25.1 + dev: false /@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0): resolution: {integrity: sha512-Q/3u/K73KUjTCnFUP97ZY+pBjQ1kPEgjOfXj/bJl8zW7GbXdkw6cwuyZk6ZTXkVgCBsYRYUzx4fvYK1jxdb9MA==} @@ -9459,6 +9479,7 @@ packages: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.28.0 + dev: false /@opentelemetry/exporter-logs-otlp-http@0.49.1(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-3QoBnIGCmEkujynUP0mK155QtOM0MSf9FNrEw7u9ieCFsoMiyatg2hPp+alEDONJ8N8wGEK+wP2q3icgXBiggw==} @@ -9486,6 +9507,7 @@ packages: '@opentelemetry/otlp-exporter-base': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-logs': 0.52.1(@opentelemetry/api@1.9.0) + dev: false /@opentelemetry/exporter-trace-otlp-grpc@0.49.1(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-Zbd7f3zF7fI2587MVhBizaW21cO/SordyrZGtMtvhoxU6n4Qb02Gx71X4+PzXH620e0+JX+Pcr9bYb1HTeVyJA==} @@ -9515,6 +9537,7 @@ packages: '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) + dev: false /@opentelemetry/exporter-trace-otlp-http@0.49.1(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-KOLtZfZvIrpGZLVvblKsiVQT7gQUZNKcUUH24Zz6Xbi7LJb9Vt6xtUZFYdR5IIjvt47PIqBKDWUQlU0o1wAsRw==} @@ -9542,6 +9565,7 @@ packages: '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) + dev: false /@opentelemetry/exporter-trace-otlp-http@0.57.0(@opentelemetry/api@1.9.0): resolution: {integrity: sha512-BJl35PSkwoMlGEOrzjCG1ih6zqZoAZJIR4xyqSKC2BqPtwuRjID0vWBaEdP9xrxxJTEIEQw+gEY/0pUgicX0ew==} @@ -9584,6 +9608,7 @@ packages: '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) + dev: false /@opentelemetry/exporter-zipkin@1.22.0(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-XcFs6rGvcTz0qW5uY7JZDYD0yNEXdekXAb6sFtnZgY/cHY6BQ09HMzOjv9SX+iaXplRDcHr1Gta7VQKM1XXM6g==} @@ -9609,6 +9634,7 @@ packages: '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 + dev: false /@opentelemetry/instrumentation-express@0.36.1(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-ltIE4kIMa+83QjW/p7oe7XCESF29w3FQ9/T1VgShdX7fzm56K2a0xfEX1vF8lnHRGERYxIWX9D086C6gJOjVGA==} @@ -9666,6 +9692,7 @@ packages: '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color + dev: false /@opentelemetry/instrumentation-http@0.49.1(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-Yib5zrW2s0V8wTeUK/B3ZtpyP4ldgXj9L3Ws/axXrW1dW0/mEFKifK50MxMQK9g5NNJQS9dWH7rvcEGZdWdQDA==} @@ -9810,6 +9837,7 @@ packages: shimmer: 1.2.1 transitivePeerDependencies: - supports-color + dev: false /@opentelemetry/otlp-exporter-base@0.49.1(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-z6sHliPqDgJU45kQatAettY9/eVF58qVPaTuejw9YWfSRqid9pXPYeegDCSdyS47KAUgAtm+nC28K3pfF27HWg==} @@ -9830,6 +9858,7 @@ packages: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.0) + dev: false /@opentelemetry/otlp-exporter-base@0.57.0(@opentelemetry/api@1.9.0): resolution: {integrity: sha512-QQl4Ngm3D6H8SDO0EM642ncTxjRsf/HDq7+IWIA0eaEK/NTsJeQ3iYJiZj3F4jkALnvyeM1kkwd+DHtqxTBx9Q==} @@ -9866,6 +9895,7 @@ packages: '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.0) + dev: false /@opentelemetry/otlp-proto-exporter-base@0.49.1(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-x1qB4EUC7KikUl2iNuxCkV8yRzrSXSyj4itfpIO674H7dhI7Zv37SFaOJTDN+8Z/F50gF2ISFH9CWQ4KCtGm2A==} @@ -9908,6 +9938,7 @@ packages: '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) protobufjs: 7.3.2 + dev: false /@opentelemetry/otlp-transformer@0.57.0(@opentelemetry/api@1.9.0): resolution: {integrity: sha512-yHX7sdwkdAmSa6Jbi3caSLDWy0PCHS1pKQeKz8AIWSyQqL7IojHKgdk9A+7eRd98Z1n9YTdwWSWLnObvIqhEhQ==} @@ -9953,6 +9984,7 @@ packages: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + dev: false /@opentelemetry/propagator-jaeger@1.22.0(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-pMLgst3QIwrUfepraH5WG7xfpJ8J3CrPKrtINK0t7kBkuu96rn+HDYQ8kt3+0FXvrZI8YJE77MCQwnJWXIrgpA==} @@ -9982,6 +10014,7 @@ packages: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + dev: false /@opentelemetry/resources@1.22.0(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-+vNeIFPH2hfcNL0AJk/ykJXoUCtR1YaDUZM+p3wZNU4Hq98gzq+7b43xbkXjadD9VhWIUQqEwXyY64q6msPj6A==} @@ -10025,6 +10058,7 @@ packages: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 + dev: false /@opentelemetry/resources@1.30.0(@opentelemetry/api@1.9.0): resolution: {integrity: sha512-5mGMjL0Uld/99t7/pcd7CuVtJbkARckLVuiOX84nO8RtLtIz0/J6EOHM2TGvPZ6F4K+XjUq13gMx14w80SVCQg==} @@ -10073,6 +10107,7 @@ packages: '@opentelemetry/api-logs': 0.52.1 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) + dev: false /@opentelemetry/sdk-logs@0.57.0(@opentelemetry/api@1.9.0): resolution: {integrity: sha512-6Kbxdu/QE9LWH7+WSLmYo3DjAq+c55TiCLXiXu6b/2m2muy5SyOG2m0MrGqetyRpfYSSbIqHmJoqNVTN3+2a9g==} @@ -10108,6 +10143,7 @@ packages: '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) lodash.merge: 4.6.2 + dev: false /@opentelemetry/sdk-metrics@1.30.0(@opentelemetry/api@1.9.0): resolution: {integrity: sha512-5kcj6APyRMvv6dEIP5plz2qfJAD4OMipBRT11u/pa1a68rHKI2Ln+iXVkAGKgx8o7CXbD7FdPypTUY88ZQgP4Q==} @@ -10166,6 +10202,7 @@ packages: '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color + dev: false /@opentelemetry/sdk-trace-base@1.22.0(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-pfTuSIpCKONC6vkTpv6VmACxD+P1woZf4q0K46nSUvXFvOFqjBYKFaAMkKD3M1mlKUUh0Oajwj35qNjMl80m1Q==} @@ -10213,6 +10250,7 @@ packages: '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 + dev: false /@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0): resolution: {integrity: sha512-RKQDaDIkV7PwizmHw+rE/FgfB2a6MBx+AEVVlAHXRG1YYxLiBpPX2KhmoB99R5vA4b72iJrjle68NDWnbrE9Dg==} @@ -10269,6 +10307,7 @@ packages: '@opentelemetry/propagator-jaeger': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) semver: 7.6.3 + dev: false /@opentelemetry/sdk-trace-web@1.22.0(@opentelemetry/api@1.4.1): resolution: {integrity: sha512-id5bUhWYg475xbm4hjwWA4PnWM4duNK1EyFRkZxa3BZNuCITwiKCLvDkVhlE9RK2kvuDOPmcRxgSbU1apF9/1w==} @@ -10292,6 +10331,7 @@ packages: '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 + dev: false /@opentelemetry/semantic-conventions@1.22.0: resolution: {integrity: sha512-CAOgFOKLybd02uj/GhCdEeeBjOS0yeoDeo/CA7ASBSmenpZHAKGB3iDm/rv3BQLcabb/OprDEsSQ1y0P8A7Siw==} @@ -10304,6 +10344,7 @@ packages: /@opentelemetry/semantic-conventions@1.28.0: resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} engines: {node: '>=14'} + dev: false /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -10335,6 +10376,7 @@ packages: /@polka/url@0.5.0: resolution: {integrity: sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==} + dev: false /@polka/url@1.0.0-next.28: resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} @@ -17208,6 +17250,7 @@ packages: /@socket.io/component-emitter@3.1.0: resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + dev: false /@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.4): resolution: {integrity: sha512-ly0cra+48hDmChxmIpnESKrc94LjRL80TEmZVscuQ/WWkRP81nNj8W8cCGMqbI4L6NCuAaPRSzZF1a9GlAxxnA==} @@ -17826,21 +17869,6 @@ packages: - supports-color dev: false - /@trigger.dev/build@4.0.0-v4-beta.7(typescript@5.5.4): - resolution: {integrity: sha512-i8gg0DUliSt1ynrok8qlaPJNYwF1NtZUG5i6kNo9TRimlFJ4XxWDTQBZeBHOqVjV1NtyX+KJczmFsFtC0GrdjQ==} - engines: {node: '>=18.20.0'} - dependencies: - '@trigger.dev/core': 4.0.0-v4-beta.7 - pkg-types: 1.1.3 - tinyglobby: 0.2.12 - tsconfck: 3.1.3(typescript@5.5.4) - transitivePeerDependencies: - - bufferutil - - supports-color - - typescript - - utf-8-validate - dev: true - /@trigger.dev/companyicons@1.5.35(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-AhY7yshwh0onlgB6EGiyjyLSzl38Cuxo4tpUJVHxs5im8gDA+fuUq7o6Vz1WetFeNXwjMqh3f+bPW7bfqR4epg==} peerDependencies: @@ -17851,84 +17879,12 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@trigger.dev/core@4.0.0-v4-beta.7: - resolution: {integrity: sha512-Esw3fSbkOs2cH6OYjYjnMUGvGSny0Wx6yngepGanEfvzTm11MFEV4cDCBRrFE5ar/bANHgxdy9jYpPBLLxADRQ==} - engines: {node: '>=18.20.0'} - dependencies: - '@bugsnag/cuid': 3.1.1 - '@electric-sql/client': 1.0.0-beta.1 - '@google-cloud/precise-date': 4.0.0 - '@jsonhero/path': 1.0.21 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.52.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.25.1 - dequal: 2.0.3 - eventsource: 3.0.5 - eventsource-parser: 3.0.0 - execa: 8.0.1 - humanize-duration: 3.27.3 - jose: 5.4.0 - nanoid: 3.3.8 - prom-client: 15.1.0 - socket.io: 4.7.4 - socket.io-client: 4.7.5 - std-env: 3.8.1 - superjson: 2.2.1 - tinyexec: 0.3.2 - zod: 3.23.8 - zod-error: 1.5.0 - zod-validation-error: 1.5.0(zod@3.23.8) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - /@trigger.dev/platform@1.0.14: resolution: {integrity: sha512-sYWzsH5oNnSTe4zhm1s0JFtvuRyAjBadScE9REN4f0AkntRG572mJHOolr0HyER4k1gS1mSK0nQScXSG5LCVIA==} dependencies: zod: 3.23.8 dev: false - /@trigger.dev/sdk@4.0.0-v4-beta.7(zod@3.23.8): - resolution: {integrity: sha512-lMUO8c9sVw4NFodUMchQP4RhTYzaFe7kqw8GPldfHEPIpzwsu8A8oe5kt8SKj2/GjrTH/+o2NazdABt7ETZWcw==} - engines: {node: '>=18.20.0'} - peerDependencies: - ai: ^4.2.0 - zod: ^3.0.0 - peerDependenciesMeta: - ai: - optional: true - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.52.1 - '@opentelemetry/semantic-conventions': 1.25.1 - '@trigger.dev/core': 4.0.0-v4-beta.7 - chalk: 5.3.0 - cronstrue: 2.21.0 - debug: 4.4.0 - evt: 2.4.13 - slug: 6.1.0 - terminal-link: 3.0.0 - ulid: 2.3.0 - uncrypto: 0.1.3 - uuid: 9.0.1 - ws: 8.18.0(bufferutil@4.0.9) - zod: 3.23.8 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /@tsconfig/node10@1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} @@ -18478,6 +18434,7 @@ packages: /@types/retry@0.12.2: resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + dev: false /@types/rimraf@4.0.5: resolution: {integrity: sha512-DTCZoIQotB2SUJnYgrEx43cQIUYOlNZz0AZPbKU4PSLYTUdML5Gox0++z4F9kQocxStrCmRNhi4x5x/UlwtKUA==} @@ -19466,6 +19423,7 @@ packages: dependencies: mime-types: 3.0.0 negotiator: 1.0.0 + dev: false /acorn-import-assertions@1.9.0(acorn@8.12.1): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} @@ -19809,6 +19767,7 @@ packages: engines: {node: '>=12'} dependencies: type-fest: 1.4.0 + dev: false /ansi-escapes@7.0.0: resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} @@ -20479,6 +20438,7 @@ packages: /bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + dev: false /bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -20525,6 +20485,7 @@ packages: type-is: 2.0.0 transitivePeerDependencies: - supports-color + dev: false /bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} @@ -20655,6 +20616,7 @@ packages: engines: {node: '>=18'} dependencies: run-applescript: 7.0.0 + dev: false /bundle-require@5.1.0(esbuild@0.25.1): resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} @@ -20708,29 +20670,6 @@ packages: rc9: 2.1.2 dev: false - /c12@1.11.1(magicast@0.3.5): - resolution: {integrity: sha512-KDU0TvSvVdaYcQKQ6iPHATGz/7p/KiVjPg4vQrB6Jg/wX9R0yl5RZxWm9IoZqaIHD2+6PZd81+KMGwRr/lRIUg==} - peerDependencies: - magicast: ^0.3.4 - peerDependenciesMeta: - magicast: - optional: true - dependencies: - chokidar: 3.6.0 - confbox: 0.1.8 - defu: 6.1.4 - dotenv: 16.4.7 - giget: 1.2.3 - jiti: 1.21.6 - magicast: 0.3.5 - mlly: 1.7.4 - ohash: 1.1.3 - pathe: 1.1.2 - perfect-debounce: 1.0.0 - pkg-types: 1.1.3 - rc9: 2.1.2 - dev: true - /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -20791,6 +20730,7 @@ packages: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 + dev: false /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} @@ -20815,6 +20755,7 @@ packages: dependencies: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + dev: false /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -21077,6 +21018,7 @@ packages: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} dependencies: consola: 3.4.2 + dev: false /cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} @@ -21340,6 +21282,7 @@ packages: /commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} + dev: false /common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} @@ -21406,6 +21349,7 @@ packages: /confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + dev: false /config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -21417,6 +21361,7 @@ packages: /consola@3.2.3: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} + dev: false /consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} @@ -21433,6 +21378,7 @@ packages: engines: {node: '>= 0.6'} dependencies: safe-buffer: 5.2.1 + dev: false /content-type@1.0.4: resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} @@ -21458,6 +21404,7 @@ packages: /cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} + dev: false /cookie@0.4.2: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} @@ -21475,6 +21422,7 @@ packages: /cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} + dev: false /cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} @@ -21485,6 +21433,7 @@ packages: engines: {node: '>=12.13'} dependencies: is-what: 4.1.16 + dev: false /copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -22057,6 +22006,7 @@ packages: /default-browser-id@5.0.0: resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} engines: {node: '>=18'} + dev: false /default-browser@5.2.1: resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} @@ -22064,6 +22014,7 @@ packages: dependencies: bundle-name: 4.1.0 default-browser-id: 5.0.0 + dev: false /defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -22090,6 +22041,7 @@ packages: /define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} + dev: false /define-properties@1.1.4: resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} @@ -22156,6 +22108,7 @@ packages: /destr@2.0.3: resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + dev: false /destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} @@ -22394,6 +22347,7 @@ packages: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 + dev: false /duplexer3@0.1.5: resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} @@ -22495,6 +22449,7 @@ packages: /encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + dev: false /encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} @@ -22519,6 +22474,7 @@ packages: - bufferutil - supports-color - utf-8-validate + dev: false /engine.io-parser@5.2.2(patch_hash=e6nctogrhpxoivwiwy37ersfu4): resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} @@ -22692,6 +22648,7 @@ packages: /es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} + dev: false /es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} @@ -22716,6 +22673,7 @@ packages: engines: {node: '>= 0.4'} dependencies: es-errors: 1.3.0 + dev: false /es-set-tostringtag@2.0.1: resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} @@ -23870,6 +23828,7 @@ packages: /event-target-shim@6.0.2: resolution: {integrity: sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==} engines: {node: '>=10.13.0'} + dev: false /eventemitter3@3.1.2: resolution: {integrity: sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==} @@ -23894,12 +23853,14 @@ packages: /eventsource-parser@3.0.0: resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==} engines: {node: '>=18.0.0'} + dev: false /eventsource@3.0.5: resolution: {integrity: sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==} engines: {node: '>=18.0.0'} dependencies: eventsource-parser: 3.0.0 + dev: false /evt@2.4.13: resolution: {integrity: sha512-haTVOsmjzk+28zpzvVwan9Zw2rLQF2izgi7BKjAPRzZAfcv+8scL0TpM8MzvGNKFYHiy+Bq3r6FYIIUPl9kt3A==} @@ -23907,6 +23868,7 @@ packages: minimal-polyfills: 2.2.2 run-exclusive: 2.2.18 tsafe: 1.4.1 + dev: false /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} @@ -23957,6 +23919,7 @@ packages: express: ^4.11 || 5 || ^5.0.0-beta.1 dependencies: express: 5.0.1 + dev: false /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} @@ -24034,6 +23997,7 @@ packages: vary: 1.1.2 transitivePeerDependencies: - supports-color + dev: false /extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -24118,6 +24082,7 @@ packages: /fast-npm-meta@0.2.2: resolution: {integrity: sha512-E+fdxeaOQGo/CMWc9f4uHFfgUPJRAu7N3uB8GBvB3SDPAIWJK4GKyYhkAGFq+GYrcbKNfQIz5VVQyJnDuPPCrg==} + dev: false /fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} @@ -24261,6 +24226,7 @@ packages: statuses: 2.0.1 transitivePeerDependencies: - supports-color + dev: false /find-cache-dir@3.3.2: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} @@ -24483,6 +24449,7 @@ packages: /fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + dev: false /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -24631,6 +24598,7 @@ packages: has-symbols: 1.1.0 hasown: 2.0.2 math-intrinsics: 1.1.0 + dev: false /get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} @@ -24647,6 +24615,7 @@ packages: dependencies: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + dev: false /get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} @@ -24735,6 +24704,7 @@ packages: ohash: 1.1.3 pathe: 1.1.2 tar: 6.2.1 + dev: false /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -24892,6 +24862,7 @@ packages: /gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + dev: false /got@9.6.0: resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} @@ -24925,6 +24896,7 @@ packages: dependencies: chalk: 4.1.2 tinygradient: 1.1.5 + dev: false /grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} @@ -25045,6 +25017,7 @@ packages: /has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + dev: false /has-tostringtag@1.0.0: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} @@ -25292,6 +25265,7 @@ packages: /humanize-duration@3.27.3: resolution: {integrity: sha512-iimHkHPfIAQ8zCDQLgn08pRqSVioyWvnGfaQ8gond2wf7Jq2jJ+24ykmnRyiz3fIldcn4oUuQXpjqKLhSVR7lw==} + dev: false /humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} @@ -25314,6 +25288,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 + dev: false /iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} @@ -25368,6 +25343,7 @@ packages: acorn-import-attributes: 1.9.5(acorn@8.12.1) cjs-module-lexer: 1.2.3 module-details-from-path: 1.0.3 + dev: false /import-in-the-middle@1.7.1: resolution: {integrity: sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg==} @@ -25388,6 +25364,7 @@ packages: /import-meta-resolve@4.1.0: resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + dev: false /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} @@ -25636,6 +25613,7 @@ packages: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + dev: false /is-electron@2.2.0: resolution: {integrity: sha512-SpMppC2XR3YdxSzczXReBjqs2zGscWQpBIKqwXYBFic0ERaxNVgwLCHwOLZeESfdJQjX0RDvrJ1lBXX2ij+G1Q==} @@ -25680,6 +25658,7 @@ packages: hasBin: true dependencies: is-docker: 3.0.0 + dev: false /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} @@ -25701,6 +25680,7 @@ packages: /is-network-error@1.0.0: resolution: {integrity: sha512-P3fxi10Aji2FZmHTrMPSNFbNC6nnp4U5juPAIjXPHkUNubi4+qK7vvdsaNpAUwXslhYm9oyjEYTxs1xd/+Ph0w==} engines: {node: '>=16'} + dev: false /is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} @@ -25751,6 +25731,7 @@ packages: /is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + dev: false /is-reference@3.0.1: resolution: {integrity: sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==} @@ -25842,6 +25823,7 @@ packages: /is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} + dev: false /is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} @@ -25859,6 +25841,7 @@ packages: engines: {node: '>=16'} dependencies: is-inside-container: 1.0.0 + dev: false /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -26002,6 +25985,7 @@ packages: /jose@5.4.0: resolution: {integrity: sha512-6rpxTHPAQyWMb9A35BroFl1Sp0ST3DpPcm5EVIxZxdH+e0Hv9fwhyB3XLKFUcHNpdSDnETmBfuPPTTlYz5+USw==} + dev: false /jose@6.0.8: resolution: {integrity: sha512-EyUPtOKyTYq+iMOszO42eobQllaIjJnwkZ2U93aJzNyPibCy7CEvT9UQnaCVB51IAd49gbNdCew1c0LcLTCB2g==} @@ -26139,6 +26123,7 @@ packages: /jsonc-parser@3.2.1: resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + dev: false /jsondiffpatch@0.6.0: resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} @@ -26911,10 +26896,12 @@ packages: engines: {node: '>=6'} dependencies: '@arr/every': 1.0.1 + dev: false /math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + dev: false /md-to-react-email@5.0.2(react@18.3.1): resolution: {integrity: sha512-x6kkpdzIzUhecda/yahltfEl53mH26QdWu4abUF9+S0Jgam8P//Ciro8cdhyMHnT5MQUJYrIbO6ORM2UxPiNNA==} @@ -27184,6 +27171,7 @@ packages: /media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} + dev: false /memorystream@0.3.1: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} @@ -27218,6 +27206,7 @@ packages: /merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + dev: false /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -27686,6 +27675,7 @@ packages: /mime-db@1.53.0: resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} engines: {node: '>= 0.6'} + dev: false /mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} @@ -27698,6 +27688,7 @@ packages: engines: {node: '>= 0.6'} dependencies: mime-db: 1.53.0 + dev: false /mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} @@ -27763,9 +27754,11 @@ packages: /minimal-polyfills@2.2.2: resolution: {integrity: sha512-eEOUq/LH/DbLWihrxUP050Wi7H/N/I2dQT98Ep6SqOpmIbk4sXOI4wqalve66QoZa+6oljbZWU6I6T4dehQGmw==} + dev: false /minimal-polyfills@2.2.3: resolution: {integrity: sha512-oxdmJ9cL+xV72h0xYxp4tP2d5/fTBpP45H8DIOn9pASuF8a3IYTf+25fMGDYGiWW+MFsuog6KD6nfmhZJQ+uUw==} + dev: false /minimatch@10.0.1: resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} @@ -27887,6 +27880,7 @@ packages: /minipass@5.0.0: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} + dev: false /minipass@7.0.3: resolution: {integrity: sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==} @@ -27955,6 +27949,7 @@ packages: pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.5.4 + dev: false /module-details-from-path@1.0.3: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} @@ -28110,6 +28105,7 @@ packages: /negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + dev: false /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -28332,6 +28328,7 @@ packages: /node-fetch-native@1.6.4: resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + dev: false /node-fetch@2.6.12: resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} @@ -28562,6 +28559,7 @@ packages: pathe: 1.1.2 pkg-types: 1.1.3 ufo: 1.5.4 + dev: false /nypm@0.5.4: resolution: {integrity: sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA==} @@ -28574,6 +28572,7 @@ packages: pkg-types: 1.3.1 tinyexec: 0.3.2 ufo: 1.5.4 + dev: false /oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} @@ -28607,6 +28606,7 @@ packages: /object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + dev: false /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} @@ -28699,6 +28699,7 @@ packages: /ohash@1.1.3: resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} + dev: false /oidc-token-hash@5.0.3: resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} @@ -28753,6 +28754,7 @@ packages: define-lazy-prop: 3.0.0 is-inside-container: 1.0.0 is-wsl: 3.1.0 + dev: false /open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} @@ -28904,6 +28906,7 @@ packages: engines: {node: '>= 4.0'} optionalDependencies: fsevents: 2.3.3 + dev: false /os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} @@ -28994,6 +28997,7 @@ packages: engines: {node: '>=18'} dependencies: yocto-queue: 1.1.1 + dev: false /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} @@ -29066,6 +29070,7 @@ packages: '@types/retry': 0.12.2 is-network-error: 1.0.0 retry: 0.13.1 + dev: false /p-timeout@3.2.0: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} @@ -29205,6 +29210,7 @@ packages: resolution: {integrity: sha512-rAFOUKImaq+VBk2B+2RTBsWEvlnarEP53nchoUHzpVs8V6fG2/estihOTslTQUWHVuHEKDL5k8htG8K3TngyFA==} dependencies: event-target-shim: 6.0.2 + dev: false /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -29267,6 +29273,7 @@ packages: /path-to-regexp@8.2.0: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} + dev: false /path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} @@ -29320,6 +29327,7 @@ packages: /perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + dev: false /performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} @@ -29464,6 +29472,7 @@ packages: /pkce-challenge@4.1.0: resolution: {integrity: sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==} engines: {node: '>=16.20.0'} + dev: false /pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} @@ -29491,6 +29500,7 @@ packages: confbox: 0.1.8 mlly: 1.7.4 pathe: 2.0.3 + dev: false /platform@1.3.6: resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} @@ -29511,6 +29521,7 @@ packages: dependencies: '@polka/url': 0.5.0 trouter: 2.0.1 + dev: false /possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} @@ -30124,6 +30135,7 @@ packages: dependencies: '@opentelemetry/api': 1.9.0 tdigest: 0.1.2 + dev: false /promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} @@ -30328,12 +30340,14 @@ packages: engines: {node: '>=0.6'} dependencies: side-channel: 1.1.0 + dev: false /qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} dependencies: side-channel: 1.1.0 + dev: false /qs@6.5.3: resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} @@ -30387,12 +30401,14 @@ packages: http-errors: 2.0.0 iconv-lite: 0.6.3 unpipe: 1.0.0 + dev: false /rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} dependencies: defu: 6.1.4 destr: 2.0.3 + dev: false /rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} @@ -31708,6 +31724,7 @@ packages: is-promise: 4.0.0 parseurl: 1.3.3 path-to-regexp: 8.2.0 + dev: false /rtl-css-js@1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} @@ -31718,6 +31735,7 @@ packages: /run-applescript@7.0.0: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} + dev: false /run-async@3.0.0: resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} @@ -31728,6 +31746,7 @@ packages: resolution: {integrity: sha512-TXr1Gkl1iEAOCCpBTRm/2m0+1KGjORcWpZZ+VGGTe7dYX8E4y8/fMvrHk0zf+kclec2R//tpvdBxgG0bDgaJfw==} dependencies: minimal-polyfills: 2.2.3 + dev: false /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -31912,6 +31931,7 @@ packages: statuses: 2.0.1 transitivePeerDependencies: - supports-color + dev: false /serialize-javascript@6.0.1: resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} @@ -31939,6 +31959,7 @@ packages: send: 1.1.0 transitivePeerDependencies: - supports-color + dev: false /server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} @@ -32062,6 +32083,7 @@ packages: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 + dev: false /side-channel-map@1.0.1: resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} @@ -32071,6 +32093,7 @@ packages: es-errors: 1.3.0 get-intrinsic: 1.3.0 object-inspect: 1.13.4 + dev: false /side-channel-weakmap@1.0.2: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} @@ -32081,6 +32104,7 @@ packages: get-intrinsic: 1.3.0 object-inspect: 1.13.4 side-channel-map: 1.0.1 + dev: false /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} @@ -32098,6 +32122,7 @@ packages: side-channel-list: 1.0.0 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + dev: false /siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -32208,6 +32233,7 @@ packages: - bufferutil - supports-color - utf-8-validate + dev: false /socket.io-client@4.7.3: resolution: {integrity: sha512-nU+ywttCyBitXIl9Xe0RSEfek4LneYkJxCeNnKCuhwoH4jGXO1ipIUw/VA/+Vvv2G1MTym11fzFC0SxkrcfXDw==} @@ -32235,6 +32261,7 @@ packages: - bufferutil - supports-color - utf-8-validate + dev: false /socket.io-parser@4.2.4: resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} @@ -32244,6 +32271,7 @@ packages: debug: 4.3.7 transitivePeerDependencies: - supports-color + dev: false /socket.io@4.7.3: resolution: {integrity: sha512-SE+UIQXBQE+GPG2oszWMlsEmWtHVqw/h1VrYJGK5/MC7CH5p58N448HwIrtREcvR4jfdOJAY4ieQfxMr55qbbw==} @@ -32277,6 +32305,7 @@ packages: - bufferutil - supports-color - utf-8-validate + dev: false /socket.io@4.7.5: resolution: {integrity: sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==} @@ -32869,6 +32898,7 @@ packages: engines: {node: '>=16'} dependencies: copy-anything: 3.0.5 + dev: false /superstruct@2.0.2: resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} @@ -33227,6 +33257,7 @@ packages: minizlib: 2.1.2 mkdirp: 1.0.4 yallist: 4.0.0 + dev: false /tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} @@ -33244,6 +33275,7 @@ packages: resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} dependencies: bintrees: 1.0.2 + dev: false /temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} @@ -33280,6 +33312,7 @@ packages: dependencies: ansi-escapes: 5.0.0 supports-hyperlinks: 2.3.0(patch_hash=xmw2etywyp5w2jf77wkqg4ob3a) + dev: false /terser-webpack-plugin@5.3.7(@swc/core@1.3.101)(esbuild@0.19.11)(webpack@5.88.2): resolution: {integrity: sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==} @@ -33426,6 +33459,7 @@ packages: /tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + dev: false /tinybench@2.3.1: resolution: {integrity: sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==} @@ -33441,6 +33475,7 @@ packages: /tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + dev: false /tinyexec@0.3.0: resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} @@ -33481,6 +33516,7 @@ packages: dependencies: '@types/tinycolor2': 1.4.3 tinycolor2: 1.6.0 + dev: false /tinypool@0.3.1: resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==} @@ -33624,75 +33660,6 @@ packages: hasBin: true dev: true - /trigger.dev@4.0.0-v4-beta.7(typescript@5.5.4): - resolution: {integrity: sha512-eEt3jKZkBjC7uYJ2J9HdXc+1waNfI/XfeC4GH0LDmWZMnxSa8yOxeVV+AFq2xS7oh+7dsDmhI1Cy2c+hxnCY0Q==} - engines: {node: '>=18.20.0'} - hasBin: true - dependencies: - '@clack/prompts': 0.10.0 - '@depot/cli': 0.0.1-cli.2.80.0 - '@modelcontextprotocol/sdk': 1.6.1 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.52.1 - '@opentelemetry/exporter-logs-otlp-http': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fetch': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.25.1 - '@trigger.dev/build': 4.0.0-v4-beta.7(typescript@5.5.4) - '@trigger.dev/core': 4.0.0-v4-beta.7 - c12: 1.11.1(magicast@0.3.5) - chalk: 5.3.0 - chokidar: 3.6.0 - cli-table3: 0.6.5 - commander: 9.5.0 - defu: 6.1.4 - dotenv: 16.4.7 - esbuild: 0.23.0 - eventsource: 3.0.5 - evt: 2.4.13 - fast-npm-meta: 0.2.2 - gradient-string: 2.0.2 - import-in-the-middle: 1.11.0 - import-meta-resolve: 4.1.0 - jsonc-parser: 3.2.1 - magicast: 0.3.5 - minimatch: 10.0.1 - mlly: 1.7.4 - nypm: 0.5.4 - object-hash: 3.0.0 - open: 10.0.3 - p-limit: 6.2.0 - p-retry: 6.1.0 - partysocket: 1.0.2 - pkg-types: 1.1.3 - polka: 0.5.2 - resolve: 1.22.8 - semver: 7.6.3 - signal-exit: 4.1.0 - socket.io-client: 4.7.5 - source-map-support: 0.5.21 - std-env: 3.8.1 - terminal-link: 3.0.0 - tiny-invariant: 1.3.1 - tinyexec: 0.3.2 - tinyglobby: 0.2.12 - ws: 8.18.0(bufferutil@4.0.9) - xdg-app-paths: 8.3.0 - zod: 3.23.8 - zod-validation-error: 1.5.0(zod@3.23.8) - transitivePeerDependencies: - - bufferutil - - supports-color - - typescript - - utf-8-validate - dev: true - /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -33709,6 +33676,7 @@ packages: engines: {node: '>=6'} dependencies: matchit: 1.1.0 + dev: false /ts-easing@0.2.0: resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} @@ -33822,6 +33790,7 @@ packages: /tsafe@1.4.1: resolution: {integrity: sha512-3IDBalvf6SyvHFS14UiwCWzqdSdo+Q0k2J7DZyJYaHW/iraW9DJpaBKDJpry3yQs3o/t/A+oGaRW3iVt2lKxzA==} + dev: false /tsconfck@2.1.2(typescript@5.5.4): resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} @@ -33847,6 +33816,7 @@ packages: optional: true dependencies: typescript: 5.5.4 + dev: false /tsconfig-paths@3.14.1: resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} @@ -34140,6 +34110,7 @@ packages: /type-fest@1.4.0: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} engines: {node: '>=10'} + dev: false /type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} @@ -34170,6 +34141,7 @@ packages: content-type: 1.0.5 media-typer: 1.1.0 mime-types: 3.0.0 + dev: false /typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} @@ -35878,6 +35850,7 @@ packages: xdg-portable: 10.6.0 optionalDependencies: fsevents: 2.3.3 + dev: false /xdg-portable@10.6.0: resolution: {integrity: sha512-xrcqhWDvtZ7WLmt8G4f3hHy37iK7D2idtosRgkeiSPZEPmBShp0VfmRBLWAPC6zLF48APJ21yfea+RfQMF4/Aw==} @@ -35886,10 +35859,12 @@ packages: os-paths: 7.4.0 optionalDependencies: fsevents: 2.3.3 + dev: false /xmlhttprequest-ssl@2.0.0: resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} engines: {node: '>=0.4.0'} + dev: false /xstate@5.18.1: resolution: {integrity: sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g==} @@ -36059,6 +36034,7 @@ packages: resolution: {integrity: sha512-zzopKZ/skI9iXpqCEPj+iLCKl9b88E43ehcU+sbRoHuwGd9F1IDVGQ70TyO6kmfiRL1g4IXkjsXK+g1gLYl4WQ==} dependencies: zod: 3.23.8 + dev: false /zod-to-json-schema@3.23.2(zod@3.23.8): resolution: {integrity: sha512-uSt90Gzc/tUfyNqxnjlfBs8W6WSGpNBv0rVsNxP/BVSMHMKGdthPYff4xtCHYloJGM0CFxFsb3NbC0eqPhfImw==} @@ -36089,6 +36065,7 @@ packages: zod: ^3.18.0 dependencies: zod: 3.23.8 + dev: false /zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} diff --git a/references/hello-world/package.json b/references/hello-world/package.json index 26e8842176..9512898e21 100644 --- a/references/hello-world/package.json +++ b/references/hello-world/package.json @@ -3,10 +3,10 @@ "private": true, "type": "module", "devDependencies": { - "trigger.dev": "4.0.0-v4-beta.7" + "trigger.dev": "workspace:*" }, "dependencies": { - "@trigger.dev/sdk": "4.0.0-v4-beta.7" + "@trigger.dev/sdk": "workspace:*" }, "scripts": { "dev": "trigger dev" From 9e2729c91ddcb3a9f5b32db4e209f08683c3649a Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 29 Apr 2025 21:39:18 +0100 Subject: [PATCH 12/62] runtime manager debug logs --- .../src/entryPoints/managed/execution.ts | 24 +++++++++ .../cli-v3/src/entryPoints/managed/logger.ts | 7 ++- .../cli-v3/src/executions/taskRunProcess.ts | 9 ++++ .../v3/runEngineWorker/supervisor/schemas.ts | 7 +-- .../src/v3/runtime/sharedRuntimeManager.ts | 54 +++++++++++++++++-- packages/core/src/v3/schemas/messages.ts | 8 +++ 6 files changed, 99 insertions(+), 10 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 927ed409fe..754bc12d22 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -144,6 +144,10 @@ export class RunExecution { } }); + taskRunProcess.onSendDebugLog.attach(async (debugLog) => { + this.sendRuntimeDebugLog(debugLog.message, debugLog.properties); + }); + return taskRunProcess; } @@ -951,6 +955,26 @@ export class RunExecution { }); } + private sendRuntimeDebugLog( + message: string, + properties?: SendDebugLogOptions["properties"], + runIdOverride?: string + ) { + this.logger.sendDebugLog({ + runId: runIdOverride ?? this.runFriendlyId, + message: `[runtime] ${message}`, + print: false, + properties: { + ...properties, + runId: this.runFriendlyId, + snapshotId: this.currentSnapshotId, + executionId: this.id, + executionRestoreCount: this.restoreCount, + lastHeartbeat: this.lastHeartbeat?.toISOString(), + }, + }); + } + // Ensure we can only set this once private set runFriendlyId(id: string) { if (this._runFriendlyId) { diff --git a/packages/cli-v3/src/entryPoints/managed/logger.ts b/packages/cli-v3/src/entryPoints/managed/logger.ts index 3a7a045476..6ae9d1b83e 100644 --- a/packages/cli-v3/src/entryPoints/managed/logger.ts +++ b/packages/cli-v3/src/entryPoints/managed/logger.ts @@ -9,6 +9,7 @@ export type SendDebugLogOptions = { message: string; date?: Date; properties?: WorkloadDebugLogRequestBody["properties"]; + print?: boolean; }; export type RunLoggerOptions = { @@ -25,7 +26,7 @@ export class RunLogger { this.env = opts.env; } - sendDebugLog({ runId, message, date, properties }: SendDebugLogOptions) { + sendDebugLog({ runId, message, date, properties, print = true }: SendDebugLogOptions) { if (!runId) { runId = this.env.TRIGGER_RUN_ID; } @@ -41,7 +42,9 @@ export class RunLogger { workerName: this.env.TRIGGER_WORKER_INSTANCE_NAME, }; - console.log(message, mergedProperties); + if (print) { + console.log(message, mergedProperties); + } this.httpClient.sendDebugLog(runId, { message, diff --git a/packages/cli-v3/src/executions/taskRunProcess.ts b/packages/cli-v3/src/executions/taskRunProcess.ts index 9e9e8283b9..b25da75e6d 100644 --- a/packages/cli-v3/src/executions/taskRunProcess.ts +++ b/packages/cli-v3/src/executions/taskRunProcess.ts @@ -33,6 +33,11 @@ import { SuspendedProcessError, } from "@trigger.dev/core/v3/errors"; +export type OnSendDebugLogMessage = InferSocketMessageSchema< + typeof ExecutorToWorkerMessageCatalog, + "SEND_DEBUG_LOG" +>; + export type TaskRunProcessOptions = { workerManifest: WorkerManifest; serverWorker: ServerBackgroundWorker; @@ -68,6 +73,7 @@ export class TaskRunProcess { public onExit: Evt<{ code: number | null; signal: NodeJS.Signals | null; pid?: number }> = new Evt(); public onIsBeingKilled: Evt = new Evt(); + public onSendDebugLog: Evt = new Evt(); private _isPreparedForNextRun: boolean = false; private _isPreparedForNextAttempt: boolean = false; @@ -178,6 +184,9 @@ export class TaskRunProcess { UNCAUGHT_EXCEPTION: async (message) => { logger.debug("uncaught exception in task run process", { ...message }); }, + SEND_DEBUG_LOG: async (message) => { + this.onSendDebugLog.post(message); + }, }, }); diff --git a/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts b/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts index abae1e28b3..8d00d6da7f 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts @@ -126,7 +126,7 @@ export type WorkerApiDequeueFromVersionResponseBody = z.infer< typeof WorkerApiDequeueFromVersionResponseBody >; -const AttributeValue = z.union([ +export const DebugLogPropertiesValue = z.union([ z.string(), z.number(), z.boolean(), @@ -135,12 +135,13 @@ const AttributeValue = z.union([ z.array(z.boolean().nullable()), ]); -const Attributes = z.record(z.string(), AttributeValue.optional()); +export const DebugLogProperties = z.record(z.string(), DebugLogPropertiesValue.optional()); +export type DebugLogProperties = z.infer; export const WorkerApiDebugLogBody = z.object({ time: z.coerce.date(), message: z.string(), - properties: Attributes.optional(), + properties: DebugLogProperties.optional(), }); export type WorkerApiDebugLogBody = z.infer; diff --git a/packages/core/src/v3/runtime/sharedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts index 2f38ec7f85..5ffa6201b5 100644 --- a/packages/core/src/v3/runtime/sharedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -1,6 +1,7 @@ import { assertExhaustive } from "../../utils.js"; import { clock } from "../clock-api.js"; import { lifecycleHooks } from "../lifecycle-hooks-api.js"; +import { DebugLogProperties } from "../runEngineWorker/index.js"; import { BatchTaskRunExecutionResult, CompletedWaitpoint, @@ -176,7 +177,10 @@ export class SharedRuntimeManager implements RuntimeManager { switch (waitpoint.type) { case "RUN": { if (!waitpoint.completedByTaskRun) { - this.log("No completedByTaskRun for RUN waitpoint", waitpoint); + this.debugLog( + "No completedByTaskRun for RUN waitpoint", + this.waitpointForDebugLog(waitpoint) + ); return null; } @@ -192,7 +196,10 @@ export class SharedRuntimeManager implements RuntimeManager { } case "BATCH": { if (!waitpoint.completedByBatch) { - this.log("No completedByBatch for BATCH waitpoint", waitpoint); + this.debugLog( + "No completedByBatch for BATCH waitpoint", + this.waitpointForDebugLog(waitpoint) + ); return null; } @@ -213,18 +220,19 @@ export class SharedRuntimeManager implements RuntimeManager { } private resolveWaitpoint(waitpoint: CompletedWaitpoint, resolverId?: ResolverId | null): void { + // This is spammy, don't make this a debug log this.log("resolveWaitpoint", waitpoint); if (waitpoint.type === "BATCH") { // We currently ignore these, they're not required to resume after a batch completes - this.log("Ignoring BATCH waitpoint", waitpoint); + this.debugLog("Ignoring BATCH waitpoint", this.waitpointForDebugLog(waitpoint)); return; } resolverId = resolverId ?? this.resolverIdFromWaitpoint(waitpoint); if (!resolverId) { - this.log("No resolverId for waitpoint", { ...this.status, ...waitpoint }); + this.debugLog("No resolverId for waitpoint", this.waitpointForDebugLog(waitpoint)); // No need to store the waitpoint, we'll never be able to resolve it return; @@ -233,7 +241,7 @@ export class SharedRuntimeManager implements RuntimeManager { const resolve = this.resolversById.get(resolverId); if (!resolve) { - this.log("No resolver found for resolverId", { ...this.status, resolverId }); + this.debugLog("No resolver found for resolverId", { ...this.status, resolverId }); // Store the waitpoint for later if we can't find a resolver this.waitpointsByResolverId.set(resolverId, waitpoint); @@ -280,6 +288,42 @@ export class SharedRuntimeManager implements RuntimeManager { } } + private waitpointForDebugLog(waitpoint: CompletedWaitpoint): DebugLogProperties { + const { + completedByTaskRun, + completedByBatch, + completedAfter, + completedAt, + output, + id, + ...rest + } = waitpoint; + + return { + ...rest, + waitpointId: id, + output: output?.slice(0, 100), + completedByTaskRunId: completedByTaskRun?.id, + completedByTaskRunBatchId: completedByTaskRun?.batch?.id, + completedByBatchId: completedByBatch?.id, + completedAfter: completedAfter?.toISOString(), + completedAt: completedAt?.toISOString(), + }; + } + + private debugLog(message: string, properties?: DebugLogProperties) { + const status = this.status; + + if (this.showLogs) { + console.log(`[${new Date().toISOString()}] ${message}`, { ...status, ...properties }); + } + + this.ipc.send("SEND_DEBUG_LOG", { + message, + properties: { ...status, ...properties }, + }); + } + private log(message: string, ...args: any[]) { if (!this.showLogs) return; console.log(`[${new Date().toISOString()}] ${message}`, args); diff --git a/packages/core/src/v3/schemas/messages.ts b/packages/core/src/v3/schemas/messages.ts index 8076af621a..18316ba408 100644 --- a/packages/core/src/v3/schemas/messages.ts +++ b/packages/core/src/v3/schemas/messages.ts @@ -18,6 +18,7 @@ import { WaitReason, } from "./schemas.js"; import { CompletedWaitpoint } from "./runEngine.js"; +import { DebugLogProperties } from "../runEngineWorker/index.js"; export const AckCallbackResult = z.discriminatedUnion("success", [ z.object({ @@ -174,6 +175,13 @@ export const ExecutorToWorkerMessageCatalog = { UNCAUGHT_EXCEPTION: { message: UncaughtExceptionMessage, }, + SEND_DEBUG_LOG: { + message: z.object({ + version: z.literal("v1").default("v1"), + message: z.string(), + properties: DebugLogProperties.optional(), + }), + }, }; export const WorkerToExecutorMessageCatalog = { From 752770f17ed9e6a0b9c824b93436e0bc7b604064 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:50:25 +0100 Subject: [PATCH 13/62] prefix engine run logs --- apps/webapp/app/v3/runEngineHandlers.server.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/v3/runEngineHandlers.server.ts b/apps/webapp/app/v3/runEngineHandlers.server.ts index 1a71157289..2767e613db 100644 --- a/apps/webapp/app/v3/runEngineHandlers.server.ts +++ b/apps/webapp/app/v3/runEngineHandlers.server.ts @@ -401,7 +401,7 @@ export function registerRunEngineEventBusHandlers() { engine.eventBus.on("executionSnapshotCreated", async ({ time, run, snapshot }) => { const eventResult = await recordRunDebugLog( run.id, - `${snapshot.executionStatus} - ${snapshot.description}`, + `[engine] ${snapshot.executionStatus} - ${snapshot.description}`, { attributes: { properties: { @@ -450,6 +450,7 @@ export function registerRunEngineEventBusHandlers() { // Record notification event const eventResult = await recordRunDebugLog( run.id, + // don't prefix this with [engine] - "run:notify" is the correct prefix `run:notify platform -> supervisor: ${snapshot.executionStatus}`, { attributes: { @@ -479,6 +480,7 @@ export function registerRunEngineEventBusHandlers() { // Record notification event const eventResult = await recordRunDebugLog( run.id, + // don't prefix this with [engine] - "run:notify" is the correct prefix `run:notify ERROR platform -> supervisor: ${snapshot.executionStatus}`, { attributes: { @@ -505,7 +507,7 @@ export function registerRunEngineEventBusHandlers() { engine.eventBus.on("incomingCheckpointDiscarded", async ({ time, run, snapshot, checkpoint }) => { const eventResult = await recordRunDebugLog( run.id, - `Checkpoint discarded: ${checkpoint.discardReason}`, + `[engine] Checkpoint discarded: ${checkpoint.discardReason}`, { attributes: { properties: { From 5f522927abd30da716d6d02410087c3c4f7f584a Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:52:21 +0100 Subject: [PATCH 14/62] managed run logger accepts nested props --- .../cli-v3/src/entryPoints/managed/logger.ts | 10 ++++++++-- .../src/v3/runEngineWorker/supervisor/schemas.ts | 16 +++++++++++++--- packages/core/src/v3/schemas/messages.ts | 4 ++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/logger.ts b/packages/cli-v3/src/entryPoints/managed/logger.ts index 6ae9d1b83e..56fcaba432 100644 --- a/packages/cli-v3/src/entryPoints/managed/logger.ts +++ b/packages/cli-v3/src/entryPoints/managed/logger.ts @@ -1,14 +1,16 @@ import { + DebugLogPropertiesInput, WorkloadDebugLogRequestBody, WorkloadHttpClient, } from "@trigger.dev/core/v3/runEngineWorker"; import { RunnerEnv } from "./env.js"; +import { flattenAttributes } from "@trigger.dev/core/v3"; export type SendDebugLogOptions = { runId?: string; message: string; date?: Date; - properties?: WorkloadDebugLogRequestBody["properties"]; + properties?: DebugLogPropertiesInput; print?: boolean; }; @@ -46,10 +48,14 @@ export class RunLogger { console.log(message, mergedProperties); } + const flattenedProperties = flattenAttributes( + mergedProperties + ) satisfies WorkloadDebugLogRequestBody["properties"]; + this.httpClient.sendDebugLog(runId, { message, time: date ?? new Date(), - properties: mergedProperties, + properties: flattenedProperties, }); } } diff --git a/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts b/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts index 8d00d6da7f..1e5025eff3 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts @@ -130,14 +130,24 @@ export const DebugLogPropertiesValue = z.union([ z.string(), z.number(), z.boolean(), - z.array(z.string().nullable()), - z.array(z.number().nullable()), - z.array(z.boolean().nullable()), + z.array(z.string().nullish()), + z.array(z.number().nullish()), + z.array(z.boolean().nullish()), ]); export const DebugLogProperties = z.record(z.string(), DebugLogPropertiesValue.optional()); export type DebugLogProperties = z.infer; +export const DebugLogPropertiesInput = z.record(z.string(), z.unknown()); +export type DebugLogPropertiesInput = z.infer; + +export const WorkerApiDebugLogBodyInput = z.object({ + time: z.coerce.date(), + message: z.string(), + properties: DebugLogPropertiesInput.optional(), +}); +export type WorkerApiDebugLogBodyInput = z.infer; + export const WorkerApiDebugLogBody = z.object({ time: z.coerce.date(), message: z.string(), diff --git a/packages/core/src/v3/schemas/messages.ts b/packages/core/src/v3/schemas/messages.ts index 18316ba408..110ad7bcf4 100644 --- a/packages/core/src/v3/schemas/messages.ts +++ b/packages/core/src/v3/schemas/messages.ts @@ -18,7 +18,7 @@ import { WaitReason, } from "./schemas.js"; import { CompletedWaitpoint } from "./runEngine.js"; -import { DebugLogProperties } from "../runEngineWorker/index.js"; +import { DebugLogPropertiesInput } from "../runEngineWorker/index.js"; export const AckCallbackResult = z.discriminatedUnion("success", [ z.object({ @@ -179,7 +179,7 @@ export const ExecutorToWorkerMessageCatalog = { message: z.object({ version: z.literal("v1").default("v1"), message: z.string(), - properties: DebugLogProperties.optional(), + properties: DebugLogPropertiesInput.optional(), }), }, }; From bb9ac5026a7684dba53aabbf4535dcd98cdde602 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 14:55:14 +0100 Subject: [PATCH 15/62] runtime suspendable state and improved logs --- .../cli-v3/src/executions/taskRunProcess.ts | 9 ++ .../src/v3/runtime/sharedRuntimeManager.ts | 91 +++++++++++-------- packages/core/src/v3/schemas/messages.ts | 6 ++ 3 files changed, 66 insertions(+), 40 deletions(-) diff --git a/packages/cli-v3/src/executions/taskRunProcess.ts b/packages/cli-v3/src/executions/taskRunProcess.ts index b25da75e6d..611c9a3556 100644 --- a/packages/cli-v3/src/executions/taskRunProcess.ts +++ b/packages/cli-v3/src/executions/taskRunProcess.ts @@ -38,6 +38,11 @@ export type OnSendDebugLogMessage = InferSocketMessageSchema< "SEND_DEBUG_LOG" >; +export type OnSetSuspendableMessage = InferSocketMessageSchema< + typeof ExecutorToWorkerMessageCatalog, + "SET_SUSPENDABLE" +>; + export type TaskRunProcessOptions = { workerManifest: WorkerManifest; serverWorker: ServerBackgroundWorker; @@ -74,6 +79,7 @@ export class TaskRunProcess { new Evt(); public onIsBeingKilled: Evt = new Evt(); public onSendDebugLog: Evt = new Evt(); + public onSetSuspendable: Evt = new Evt(); private _isPreparedForNextRun: boolean = false; private _isPreparedForNextAttempt: boolean = false; @@ -187,6 +193,9 @@ export class TaskRunProcess { SEND_DEBUG_LOG: async (message) => { this.onSendDebugLog.post(message); }, + SET_SUSPENDABLE: async (message) => { + this.onSetSuspendable.post(message); + }, }, }); diff --git a/packages/core/src/v3/runtime/sharedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts index 5ffa6201b5..acf734c05d 100644 --- a/packages/core/src/v3/runtime/sharedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -1,7 +1,7 @@ import { assertExhaustive } from "../../utils.js"; import { clock } from "../clock-api.js"; import { lifecycleHooks } from "../lifecycle-hooks-api.js"; -import { DebugLogProperties } from "../runEngineWorker/index.js"; +import { DebugLogPropertiesInput } from "../runEngineWorker/index.js"; import { BatchTaskRunExecutionResult, CompletedWaitpoint, @@ -11,6 +11,7 @@ import { TaskRunSuccessfulExecutionResult, WaitpointTokenResult, } from "../schemas/index.js"; +import { tryCatch } from "../tryCatch.js"; import { ExecutorToWorkerProcessConnection } from "../zodIpc.js"; import { RuntimeManager } from "./manager.js"; import { preventMultipleWaits } from "./preventMultipleWaits.js"; @@ -58,9 +59,7 @@ export class SharedRuntimeManager implements RuntimeManager { runId: params.id, }); - // FIXME: We need to send a "ready to checkpoint" signal here - - const waitpoint = await promise; + const waitpoint = await this.suspendable(promise); const result = this.waitpointToTaskRunExecutionResult(waitpoint); await lifecycleHooks.callOnResumeHookListeners({ @@ -99,9 +98,7 @@ export class SharedRuntimeManager implements RuntimeManager { runCount: params.runCount, }); - // FIXME: We need to send a "ready to checkpoint" signal here - - const waitpoints = await Promise.all(promises); + const waitpoints = await this.suspendable(Promise.all(promises)); await lifecycleHooks.callOnResumeHookListeners({ type: "batch", @@ -143,9 +140,7 @@ export class SharedRuntimeManager implements RuntimeManager { }); } - // FIXME: We need to send a "ready to checkpoint" signal here - - const waitpoint = await promise; + const waitpoint = await this.suspendable(promise); if (finishDate) { await lifecycleHooks.callOnResumeHookListeners({ @@ -177,10 +172,9 @@ export class SharedRuntimeManager implements RuntimeManager { switch (waitpoint.type) { case "RUN": { if (!waitpoint.completedByTaskRun) { - this.debugLog( - "No completedByTaskRun for RUN waitpoint", - this.waitpointForDebugLog(waitpoint) - ); + this.debugLog("no completedByTaskRun for RUN waitpoint", { + waitpoint: this.waitpointForDebugLog(waitpoint), + }); return null; } @@ -196,10 +190,9 @@ export class SharedRuntimeManager implements RuntimeManager { } case "BATCH": { if (!waitpoint.completedByBatch) { - this.debugLog( - "No completedByBatch for BATCH waitpoint", - this.waitpointForDebugLog(waitpoint) - ); + this.debugLog("no completedByBatch for BATCH waitpoint", { + waitpoint: this.waitpointForDebugLog(waitpoint), + }); return null; } @@ -225,14 +218,18 @@ export class SharedRuntimeManager implements RuntimeManager { if (waitpoint.type === "BATCH") { // We currently ignore these, they're not required to resume after a batch completes - this.debugLog("Ignoring BATCH waitpoint", this.waitpointForDebugLog(waitpoint)); + this.debugLog("ignoring BATCH waitpoint", { + waitpoint: this.waitpointForDebugLog(waitpoint), + }); return; } resolverId = resolverId ?? this.resolverIdFromWaitpoint(waitpoint); if (!resolverId) { - this.debugLog("No resolverId for waitpoint", this.waitpointForDebugLog(waitpoint)); + this.debugLog("no resolverId for waitpoint", { + waitpoint: this.waitpointForDebugLog(waitpoint), + }); // No need to store the waitpoint, we'll never be able to resolve it return; @@ -241,7 +238,10 @@ export class SharedRuntimeManager implements RuntimeManager { const resolve = this.resolversById.get(resolverId); if (!resolve) { - this.debugLog("No resolver found for resolverId", { ...this.status, resolverId }); + this.debugLog("no resolver found for resolverId", { + resolverId, + waitpoint: this.waitpointForDebugLog(waitpoint), + }); // Store the waitpoint for later if we can't find a resolver this.waitpointsByResolverId.set(resolverId, waitpoint); @@ -264,6 +264,23 @@ export class SharedRuntimeManager implements RuntimeManager { } } + private setSuspendable(suspendable: boolean): void { + this.ipc.send("SET_SUSPENDABLE", { suspendable }); + } + + private async suspendable(promise: Promise): Promise { + this.setSuspendable(true); + const [error, result] = await tryCatch(promise); + this.setSuspendable(false); + + if (error) { + this.debugLog("error in suspendable wrapper", { error: String(error) }); + throw error; + } + + return result; + } + private waitpointToTaskRunExecutionResult(waitpoint: CompletedWaitpoint): TaskRunExecutionResult { if (!waitpoint.completedByTaskRun?.friendlyId) throw new Error("Missing completedByTaskRun"); @@ -288,39 +305,33 @@ export class SharedRuntimeManager implements RuntimeManager { } } - private waitpointForDebugLog(waitpoint: CompletedWaitpoint): DebugLogProperties { - const { - completedByTaskRun, - completedByBatch, - completedAfter, - completedAt, - output, - id, - ...rest - } = waitpoint; + private waitpointForDebugLog(waitpoint: CompletedWaitpoint): DebugLogPropertiesInput { + const { completedAfter, completedAt, output, ...rest } = waitpoint; return { ...rest, - waitpointId: id, output: output?.slice(0, 100), - completedByTaskRunId: completedByTaskRun?.id, - completedByTaskRunBatchId: completedByTaskRun?.batch?.id, - completedByBatchId: completedByBatch?.id, completedAfter: completedAfter?.toISOString(), completedAt: completedAt?.toISOString(), + completedAfterDate: completedAfter, + completedAtDate: completedAt, }; } - private debugLog(message: string, properties?: DebugLogProperties) { - const status = this.status; - + private debugLog(message: string, properties?: DebugLogPropertiesInput) { if (this.showLogs) { - console.log(`[${new Date().toISOString()}] ${message}`, { ...status, ...properties }); + console.log(`[${new Date().toISOString()}] ${message}`, { + runtimeStatus: this.status, + ...properties, + }); } this.ipc.send("SEND_DEBUG_LOG", { message, - properties: { ...status, ...properties }, + properties: { + runtimeStatus: this.status, + ...properties, + }, }); } diff --git a/packages/core/src/v3/schemas/messages.ts b/packages/core/src/v3/schemas/messages.ts index 110ad7bcf4..ec4048d2e3 100644 --- a/packages/core/src/v3/schemas/messages.ts +++ b/packages/core/src/v3/schemas/messages.ts @@ -182,6 +182,12 @@ export const ExecutorToWorkerMessageCatalog = { properties: DebugLogPropertiesInput.optional(), }), }, + SET_SUSPENDABLE: { + message: z.object({ + version: z.literal("v1").default("v1"), + suspendable: z.boolean(), + }), + }, }; export const WorkerToExecutorMessageCatalog = { From 3d978d337a7fab674c9bfe2fdd0fe6ea12954db6 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:54:02 +0100 Subject: [PATCH 16/62] require suspendable state for checkpoints, fix snapshot processing queue --- .../src/entryPoints/managed/controller.ts | 64 ++- .../cli-v3/src/entryPoints/managed/env.ts | 4 - .../src/entryPoints/managed/execution.ts | 404 +++++++++--------- .../cli-v3/src/entryPoints/managed/poller.ts | 1 + .../entryPoints/managed/snapshotManager.ts | 280 ++++++++++++ 5 files changed, 526 insertions(+), 227 deletions(-) create mode 100644 packages/cli-v3/src/entryPoints/managed/snapshotManager.ts diff --git a/packages/cli-v3/src/entryPoints/managed/controller.ts b/packages/cli-v3/src/entryPoints/managed/controller.ts index f73c313d72..a459ccc7b4 100644 --- a/packages/cli-v3/src/entryPoints/managed/controller.ts +++ b/packages/cli-v3/src/entryPoints/managed/controller.ts @@ -401,46 +401,64 @@ export class ManagedRunController { }) satisfies SupervisorSocket; socket.on("run:notify", async ({ version, run }) => { + // Generate a unique ID for the notification + const notificationId = Math.random().toString(36).substring(2, 15); + + // Use this to track the notification incl. any processing + const notification = { + id: notificationId, + runId: run.friendlyId, + version, + }; + + // Lock this to the current run and snapshot IDs + const controller = { + runFriendlyId: this.runFriendlyId, + snapshotFriendlyId: this.snapshotFriendlyId, + }; + this.sendDebugLog({ runId: run.friendlyId, message: "run:notify received by runner", - properties: { version, runId: run.friendlyId }, + properties: { + notification, + controller, + }, }); - if (!this.runFriendlyId) { + if (!controller.runFriendlyId) { this.sendDebugLog({ runId: run.friendlyId, message: "run:notify: ignoring notification, no local run ID", properties: { - currentRunId: this.runFriendlyId, - currentSnapshotId: this.snapshotFriendlyId, + notification, + controller, }, }); return; } - if (run.friendlyId !== this.runFriendlyId) { + if (run.friendlyId !== controller.runFriendlyId) { this.sendDebugLog({ runId: run.friendlyId, message: "run:notify: ignoring notification for different run", properties: { - currentRunId: this.runFriendlyId, - currentSnapshotId: this.snapshotFriendlyId, - notificationRunId: run.friendlyId, + notification, + controller, }, }); return; } - const latestSnapshot = await this.httpClient.getRunExecutionData(this.runFriendlyId); + const latestSnapshot = await this.httpClient.getRunExecutionData(controller.runFriendlyId); if (!latestSnapshot.success) { this.sendDebugLog({ - runId: this.runFriendlyId, + runId: run.friendlyId, message: "run:notify: failed to get latest snapshot data", properties: { - currentRunId: this.runFriendlyId, - currentSnapshotId: this.snapshotFriendlyId, + notification, + controller, error: latestSnapshot.error, }, }); @@ -451,19 +469,29 @@ export class ManagedRunController { if (!this.currentExecution) { this.sendDebugLog({ - runId: runExecutionData.run.friendlyId, - message: "handleSnapshotChange: no current execution", + runId: run.friendlyId, + message: "run:notify: no current execution", + properties: { + notification, + controller, + }, }); return; } - const [error] = await tryCatch(this.currentExecution.handleSnapshotChange(runExecutionData)); + const [error] = await tryCatch( + this.currentExecution.enqueueSnapshotChangeAndWait(runExecutionData) + ); if (error) { this.sendDebugLog({ - runId: runExecutionData.run.friendlyId, - message: "handleSnapshotChange: unexpected error", - properties: { error: error.message }, + runId: run.friendlyId, + message: "run:notify: unexpected error", + properties: { + notification, + controller, + error: error.message, + }, }); } }); diff --git a/packages/cli-v3/src/entryPoints/managed/env.ts b/packages/cli-v3/src/entryPoints/managed/env.ts index 8f03968084..d3e381fa26 100644 --- a/packages/cli-v3/src/entryPoints/managed/env.ts +++ b/packages/cli-v3/src/entryPoints/managed/env.ts @@ -32,7 +32,6 @@ const Env = z.object({ TRIGGER_MACHINE_MEMORY: z.string().default("0"), TRIGGER_RUNNER_ID: z.string(), TRIGGER_METADATA_URL: z.string().optional(), - TRIGGER_PRE_SUSPEND_WAIT_MS: z.coerce.number().default(200), // Timeline metrics TRIGGER_POD_SCHEDULED_AT_MS: DateEnv, @@ -119,9 +118,6 @@ export class RunnerEnv { get TRIGGER_METADATA_URL() { return this.env.TRIGGER_METADATA_URL; } - get TRIGGER_PRE_SUSPEND_WAIT_MS() { - return this.env.TRIGGER_PRE_SUSPEND_WAIT_MS; - } get TRIGGER_POD_SCHEDULED_AT_MS() { return this.env.TRIGGER_POD_SCHEDULED_AT_MS; } diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 754bc12d22..2b13885e07 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -5,6 +5,7 @@ import { type TaskRunExecutionMetrics, type TaskRunExecutionResult, TaskRunExecutionRetry, + TaskRunExecutionStatus, type TaskRunFailedExecutionResult, WorkerManifest, } from "@trigger.dev/core/v3"; @@ -18,6 +19,7 @@ import { RunExecutionSnapshotPoller } from "./poller.js"; import { assertExhaustive, tryCatch } from "@trigger.dev/core/utils"; import { MetadataClient } from "./overrides.js"; import { randomBytes } from "node:crypto"; +import { SnapshotManager, SnapshotState } from "./snapshotManager.js"; class ExecutionAbortError extends Error { constructor(message: string) { @@ -50,9 +52,9 @@ export class RunExecution { private executionAbortController: AbortController; private _runFriendlyId?: string; - private currentSnapshotId?: string; private currentAttemptNumber?: number; private currentTaskRunEnv?: Record; + private snapshotManager?: SnapshotManager; private dequeuedAt?: Date; private podScheduledAt?: Date; @@ -148,6 +150,10 @@ export class RunExecution { this.sendRuntimeDebugLog(debugLog.message, debugLog.properties); }); + taskRunProcess.onSetSuspendable.attach(async ({ suspendable }) => { + this.suspendable = suspendable; + }); + return taskRunProcess; } @@ -165,71 +171,22 @@ export class RunExecution { /** * Called by the RunController when it receives a websocket notification - * or when the snapshot poller detects a change + * or when the snapshot poller detects a change. + * + * This is the main entry point for snapshot changes, but processing is deferred to the snapshot manager. */ - public async handleSnapshotChange(runData: RunExecutionData): Promise { + public async enqueueSnapshotChangeAndWait(runData: RunExecutionData): Promise { if (this.isShuttingDown) { - this.sendDebugLog("handleSnapshotChange: shutting down, skipping"); - return; - } - - const { run, snapshot, completedWaitpoints } = runData; - - const snapshotMetadata = { - incomingRunId: run.friendlyId, - incomingSnapshotId: snapshot.friendlyId, - completedWaitpoints: completedWaitpoints.length, - }; - - // Ensure we have run details - if (!this.runFriendlyId || !this.currentSnapshotId) { - this.sendDebugLog( - "handleSnapshotChange: missing run or snapshot ID", - snapshotMetadata, - run.friendlyId - ); + this.sendDebugLog("enqueueSnapshotChangeAndWait: shutting down, skipping"); return; } - // Ensure the run ID matches - if (run.friendlyId !== this.runFriendlyId) { - // Send debug log to both runs - this.sendDebugLog("handleSnapshotChange: mismatched run IDs", snapshotMetadata); - this.sendDebugLog( - "handleSnapshotChange: mismatched run IDs", - snapshotMetadata, - run.friendlyId - ); + if (!this.snapshotManager) { + this.sendDebugLog("enqueueSnapshotChangeAndWait: missing snapshot manager"); return; } - this.snapshotChangeQueue.push(runData); - await this.processSnapshotChangeQueue(); - } - - private snapshotChangeQueue: RunExecutionData[] = []; - private snapshotChangeQueueLock = false; - - private async processSnapshotChangeQueue() { - if (this.snapshotChangeQueueLock) { - return; - } - - this.snapshotChangeQueueLock = true; - while (this.snapshotChangeQueue.length > 0) { - const runData = this.snapshotChangeQueue.shift(); - - if (!runData) { - continue; - } - - const [error] = await tryCatch(this.processSnapshotChange(runData)); - - if (error) { - this.sendDebugLog("Failed to process snapshot change", { error: error.message }); - } - } - this.snapshotChangeQueueLock = false; + await this.snapshotManager.handleSnapshotChange(runData); } private async processSnapshotChange(runData: RunExecutionData): Promise { @@ -240,21 +197,13 @@ export class RunExecution { completedWaitpoints: completedWaitpoints.length, }; - // Check if the incoming snapshot is newer than the current one - if (!this.currentSnapshotId || snapshot.friendlyId < this.currentSnapshotId) { - this.sendDebugLog( - "handleSnapshotChange: received older snapshot, skipping", - snapshotMetadata - ); - return; - } - - if (snapshot.friendlyId === this.currentSnapshotId) { + if (!this.snapshotManager) { + this.sendDebugLog("handleSnapshotChange: missing snapshot manager", snapshotMetadata); return; } if (this.currentAttemptNumber && this.currentAttemptNumber !== run.attemptNumber) { - this.sendDebugLog("ERROR: attempt number mismatch", snapshotMetadata); + this.sendDebugLog("error: attempt number mismatch", snapshotMetadata); await this.taskRunProcess?.suspend(); return; } @@ -264,12 +213,6 @@ export class RunExecution { // Reset the snapshot poll interval so we don't do unnecessary work this.snapshotPoller?.resetCurrentInterval(); - // Update internal state - this.currentSnapshotId = snapshot.friendlyId; - - // Update services - this.snapshotPoller?.updateSnapshotId(snapshot.friendlyId); - switch (snapshot.executionStatus) { case "PENDING_CANCEL": { const [error] = await tryCatch(this.cancel()); @@ -285,14 +228,14 @@ export class RunExecution { return; } case "QUEUED": { - this.sendDebugLog("Run was re-queued", snapshotMetadata); + this.sendDebugLog("run was re-queued", snapshotMetadata); // Pretend we've just suspended the run. This will kill the process without failing the run. await this.taskRunProcess?.suspend(); return; } case "FINISHED": { - this.sendDebugLog("Run is finished", snapshotMetadata); + this.sendDebugLog("run is finished", snapshotMetadata); // Pretend we've just suspended the run. This will kill the process without failing the run. await this.taskRunProcess?.suspend(); @@ -300,80 +243,13 @@ export class RunExecution { } case "QUEUED_EXECUTING": case "EXECUTING_WITH_WAITPOINTS": { - this.sendDebugLog("Run is executing with waitpoints", snapshotMetadata); - - const [error] = await tryCatch(this.taskRunProcess?.cleanup(false)); + this.sendDebugLog("run is executing with waitpoints", snapshotMetadata); - if (error) { - this.sendDebugLog("Failed to cleanup task run process, carrying on", { - ...snapshotMetadata, - error: error.message, - }); - } - - if (snapshot.friendlyId !== this.currentSnapshotId) { - this.sendDebugLog("Snapshot changed after cleanup, abort", snapshotMetadata); - - this.abortExecution(); - return; - } - - await sleep(this.env.TRIGGER_PRE_SUSPEND_WAIT_MS); - - if (snapshot.friendlyId !== this.currentSnapshotId) { - this.sendDebugLog("Snapshot changed after suspend threshold, abort", snapshotMetadata); - - this.abortExecution(); - return; - } - - if (!this.runFriendlyId || !this.currentSnapshotId) { - this.sendDebugLog( - "handleSnapshotChange: Missing run ID or snapshot ID after suspension, abort", - snapshotMetadata - ); - - this.abortExecution(); - return; - } - - const suspendResult = await this.httpClient.suspendRun( - this.runFriendlyId, - this.currentSnapshotId - ); - - if (!suspendResult.success) { - this.sendDebugLog("Failed to suspend run, staying alive 🎶", { - ...snapshotMetadata, - error: suspendResult.error, - }); - - this.sendDebugLog("checkpoint: suspend request failed", { - ...snapshotMetadata, - error: suspendResult.error, - }); - - // This is fine, we'll wait for the next status change - return; - } - - if (!suspendResult.data.ok) { - this.sendDebugLog("checkpoint: failed to suspend run", { - snapshotId: this.currentSnapshotId, - error: suspendResult.data.error, - }); - - // This is fine, we'll wait for the next status change - return; - } - - this.sendDebugLog("Suspending, any day now 🚬", snapshotMetadata); - - // Wait for next status change + // Wait for next status change - suspension is handled by the snapshot manager return; } case "SUSPENDED": { - this.sendDebugLog("Run was suspended, kill the process", snapshotMetadata); + this.sendDebugLog("run was suspended, kill the process", snapshotMetadata); // This will kill the process and fail the execution with a SuspendedProcessError await this.taskRunProcess?.suspend(); @@ -381,17 +257,17 @@ export class RunExecution { return; } case "PENDING_EXECUTING": { - this.sendDebugLog("Run is pending execution", snapshotMetadata); + this.sendDebugLog("run is pending execution", snapshotMetadata); if (completedWaitpoints.length === 0) { - this.sendDebugLog("No waitpoints to complete, nothing to do", snapshotMetadata); + this.sendDebugLog("no waitpoints to complete, nothing to do", snapshotMetadata); return; } const [error] = await tryCatch(this.restore()); if (error) { - this.sendDebugLog("Failed to restore execution", { + this.sendDebugLog("failed to restore execution", { ...snapshotMetadata, error: error.message, }); @@ -403,16 +279,16 @@ export class RunExecution { return; } case "EXECUTING": { - this.sendDebugLog("Run is now executing", snapshotMetadata); + this.sendDebugLog("run is now executing", snapshotMetadata); if (completedWaitpoints.length === 0) { return; } - this.sendDebugLog("Processing completed waitpoints", snapshotMetadata); + this.sendDebugLog("processing completed waitpoints", snapshotMetadata); if (!this.taskRunProcess) { - this.sendDebugLog("No task run process, ignoring completed waitpoints", snapshotMetadata); + this.sendDebugLog("no task run process, ignoring completed waitpoints", snapshotMetadata); this.abortExecution(); return; @@ -425,7 +301,7 @@ export class RunExecution { return; } case "RUN_CREATED": { - this.sendDebugLog("Invalid status change", snapshotMetadata); + this.sendDebugLog("invalid status change", snapshotMetadata); this.abortExecution(); return; @@ -441,11 +317,11 @@ export class RunExecution { }: { isWarmStart?: boolean; }): Promise { - if (!this.runFriendlyId || !this.currentSnapshotId) { - throw new Error("Cannot start attempt: missing run or snapshot ID"); + if (!this.runFriendlyId || !this.snapshotManager) { + throw new Error("Cannot start attempt: missing run or snapshot manager"); } - this.sendDebugLog("Starting attempt"); + this.sendDebugLog("starting attempt"); const attemptStartedAt = Date.now(); @@ -456,7 +332,7 @@ export class RunExecution { const start = await this.httpClient.startRunAttempt( this.runFriendlyId, - this.currentSnapshotId, + this.snapshotManager.snapshotId, { isWarmStart } ); @@ -469,14 +345,17 @@ export class RunExecution { } // A snapshot was just created, so update the snapshot ID - this.currentSnapshotId = start.data.snapshot.friendlyId; + this.snapshotManager.updateSnapshot( + start.data.snapshot.friendlyId, + start.data.snapshot.executionStatus + ); // Also set or update the attempt number - we do this to detect illegal attempt number changes, e.g. from stalled runners coming back online const attemptNumber = start.data.run.attemptNumber; if (attemptNumber && attemptNumber > 0) { this.currentAttemptNumber = attemptNumber; } else { - this.sendDebugLog("ERROR: invalid attempt number returned from start attempt", { + this.sendDebugLog("error: invalid attempt number returned from start attempt", { attemptNumber: String(attemptNumber), }); } @@ -487,7 +366,7 @@ export class RunExecution { podScheduledAt: this.podScheduledAt?.getTime(), }); - this.sendDebugLog("Started attempt"); + this.sendDebugLog("started attempt"); return { ...start.data, metrics }; } @@ -499,18 +378,29 @@ export class RunExecution { public async execute(runOpts: RunExecutionRunOptions): Promise { // Setup initial state this.runFriendlyId = runOpts.runFriendlyId; - this.currentSnapshotId = runOpts.snapshotFriendlyId; + + // Create snapshot manager + this.snapshotManager = new SnapshotManager({ + runFriendlyId: runOpts.runFriendlyId, + initialSnapshotId: runOpts.snapshotFriendlyId, + // We're just guessing here, but "PENDING_EXECUTING" is probably fine + initialStatus: "PENDING_EXECUTING", + logger: this.logger, + onSnapshotChange: this.processSnapshotChange.bind(this), + onSuspendable: this.handleSuspendable.bind(this), + }); + this.dequeuedAt = runOpts.dequeuedAt; this.podScheduledAt = runOpts.podScheduledAt; // Create and start services this.snapshotPoller = new RunExecutionSnapshotPoller({ runFriendlyId: this.runFriendlyId, - snapshotFriendlyId: this.currentSnapshotId, + snapshotFriendlyId: this.snapshotManager.snapshotId, httpClient: this.httpClient, logger: this.logger, snapshotPollIntervalSeconds: this.env.TRIGGER_SNAPSHOT_POLL_INTERVAL_SECONDS, - handleSnapshotChange: this.handleSnapshotChange.bind(this), + handleSnapshotChange: this.enqueueSnapshotChangeAndWait.bind(this), }); this.snapshotPoller.start(); @@ -520,7 +410,7 @@ export class RunExecution { ); if (startError) { - this.sendDebugLog("Failed to start attempt", { error: startError.message }); + this.sendDebugLog("failed to start attempt", { error: startError.message }); this.stopServices(); return; @@ -529,7 +419,7 @@ export class RunExecution { const [executeError] = await tryCatch(this.executeRunWrapper(start)); if (executeError) { - this.sendDebugLog("Failed to execute run", { error: executeError.message }); + this.sendDebugLog("failed to execute run", { error: executeError.message }); this.stopServices(); return; @@ -562,7 +452,7 @@ export class RunExecution { }) ); - this.sendDebugLog("Run execution completed", { error: executeError?.message }); + this.sendDebugLog("run execution completed", { error: executeError?.message }); if (!executeError) { this.stopServices(); @@ -570,7 +460,7 @@ export class RunExecution { } if (executeError instanceof SuspendedProcessError) { - this.sendDebugLog("Run was suspended", { + this.sendDebugLog("run was suspended", { run: run.friendlyId, snapshot: snapshot.friendlyId, error: executeError.message, @@ -580,7 +470,7 @@ export class RunExecution { } if (executeError instanceof ExecutionAbortError) { - this.sendDebugLog("Run was interrupted", { + this.sendDebugLog("run was interrupted", { run: run.friendlyId, snapshot: snapshot.friendlyId, error: executeError.message, @@ -589,7 +479,7 @@ export class RunExecution { return; } - this.sendDebugLog("Error while executing attempt", { + this.sendDebugLog("error while executing attempt", { error: executeError.message, runId: run.friendlyId, snapshotId: snapshot.friendlyId, @@ -605,7 +495,7 @@ export class RunExecution { const [completeError] = await tryCatch(this.complete({ completion })); if (completeError) { - this.sendDebugLog("Failed to complete run", { error: completeError.message }); + this.sendDebugLog("failed to complete run", { error: completeError.message }); } this.stopServices(); @@ -641,7 +531,7 @@ export class RunExecution { // Set up an abort handler that will cleanup the task run process this.executionAbortController.signal.addEventListener("abort", async () => { - this.sendDebugLog("Execution aborted during task run, cleaning up process", { + this.sendDebugLog("execution aborted during task run, cleaning up process", { runId: execution.run.id, }); @@ -662,13 +552,13 @@ export class RunExecution { ); // If we get here, the task completed normally - this.sendDebugLog("Completed run attempt", { attemptSuccess: completion.ok }); + this.sendDebugLog("completed run attempt", { attemptSuccess: completion.ok }); // The execution has finished, so we can cleanup the task run process. Killing it should be safe. const [error] = await tryCatch(this.taskRunProcess.cleanup(true)); if (error) { - this.sendDebugLog("Failed to cleanup task run process, submitting completion anyway", { + this.sendDebugLog("failed to cleanup task run process, submitting completion anyway", { error: error.message, }); } @@ -676,7 +566,7 @@ export class RunExecution { const [completionError] = await tryCatch(this.complete({ completion })); if (completionError) { - this.sendDebugLog("Failed to complete run", { error: completionError.message }); + this.sendDebugLog("failed to complete run", { error: completionError.message }); } } @@ -700,13 +590,13 @@ export class RunExecution { } private async complete({ completion }: { completion: TaskRunExecutionResult }): Promise { - if (!this.runFriendlyId || !this.currentSnapshotId) { - throw new Error("Cannot complete run: missing run or snapshot ID"); + if (!this.runFriendlyId || !this.snapshotManager) { + throw new Error("cannot complete run: missing run or snapshot manager"); } const completionResult = await this.httpClient.completeRunAttempt( this.runFriendlyId, - this.currentSnapshotId, + this.snapshotManager.snapshotId, { completion } ); @@ -727,32 +617,33 @@ export class RunExecution { completion: TaskRunExecutionResult; result: CompleteRunAttemptResult; }) { - this.sendDebugLog("Handling completion result", { + this.sendDebugLog(`completion result: ${result.attemptStatus}`, { attemptSuccess: completion.ok, attemptStatus: result.attemptStatus, snapshotId: result.snapshot.friendlyId, runId: result.run.friendlyId, }); - // Update our snapshot ID to match the completion result - // This ensures any subsequent API calls use the correct snapshot - this.currentSnapshotId = result.snapshot.friendlyId; + const snapshotStatus = this.convertAttemptStatusToSnapshotStatus(result.attemptStatus); + + // Update our snapshot ID to match the completion result to ensure any subsequent API calls use the correct snapshot + this.updateSnapshot(result.snapshot.friendlyId, snapshotStatus); const { attemptStatus } = result; if (attemptStatus === "RUN_FINISHED") { - this.sendDebugLog("Run finished"); + this.sendDebugLog("run finished"); return; } if (attemptStatus === "RUN_PENDING_CANCEL") { - this.sendDebugLog("Run pending cancel"); + this.sendDebugLog("run pending cancel"); return; } if (attemptStatus === "RETRY_QUEUED") { - this.sendDebugLog("Retry queued"); + this.sendDebugLog("retry queued"); return; } @@ -773,6 +664,28 @@ export class RunExecution { assertExhaustive(attemptStatus); } + private updateSnapshot(snapshotId: string, status: TaskRunExecutionStatus) { + this.snapshotManager?.updateSnapshot(snapshotId, status); + this.snapshotPoller?.updateSnapshotId(snapshotId); + } + + private convertAttemptStatusToSnapshotStatus( + attemptStatus: CompleteRunAttemptResult["attemptStatus"] + ): TaskRunExecutionStatus { + switch (attemptStatus) { + case "RUN_FINISHED": + return "FINISHED"; + case "RUN_PENDING_CANCEL": + return "PENDING_CANCEL"; + case "RETRY_QUEUED": + return "QUEUED"; + case "RETRY_IMMEDIATELY": + return "EXECUTING"; + default: + assertExhaustive(attemptStatus); + } + } + private measureExecutionMetrics({ attemptCreatedAt, dequeuedAt, @@ -813,7 +726,7 @@ export class RunExecution { } private async retryImmediately({ retryOpts }: { retryOpts: TaskRunExecutionRetry }) { - this.sendDebugLog("Retrying run immediately", { + this.sendDebugLog("retrying run immediately", { timestamp: retryOpts.timestamp, delay: retryOpts.delay, }); @@ -829,7 +742,7 @@ export class RunExecution { const [startError, start] = await tryCatch(this.startAttempt({ isWarmStart: true })); if (startError) { - this.sendDebugLog("Failed to start attempt for retry", { error: startError.message }); + this.sendDebugLog("failed to start attempt for retry", { error: startError.message }); this.stopServices(); return; @@ -838,7 +751,7 @@ export class RunExecution { const [executeError] = await tryCatch(this.executeRunWrapper({ ...start, isWarmStart: true })); if (executeError) { - this.sendDebugLog("Failed to execute run for retry", { error: executeError.message }); + this.sendDebugLog("failed to execute run for retry", { error: executeError.message }); this.stopServices(); return; @@ -851,10 +764,10 @@ export class RunExecution { * Restores a suspended execution from PENDING_EXECUTING */ private async restore(): Promise { - this.sendDebugLog("Restoring execution"); + this.sendDebugLog("restoring execution"); - if (!this.runFriendlyId || !this.currentSnapshotId) { - throw new Error("Cannot restore: missing run or snapshot ID"); + if (!this.runFriendlyId || !this.snapshotManager) { + throw new Error("Cannot restore: missing run or snapshot manager"); } // Short delay to give websocket time to reconnect @@ -865,7 +778,7 @@ export class RunExecution { const continuationResult = await this.httpClient.continueRunExecution( this.runFriendlyId, - this.currentSnapshotId + this.snapshotManager.snapshotId ); if (!continuationResult.success) { @@ -881,7 +794,7 @@ export class RunExecution { */ private async processEnvOverrides() { if (!this.env.TRIGGER_METADATA_URL) { - this.sendDebugLog("No metadata URL, skipping env overrides"); + this.sendDebugLog("no metadata url, skipping env overrides"); return; } @@ -889,11 +802,11 @@ export class RunExecution { const overrides = await metadataClient.getEnvOverrides(); if (!overrides) { - this.sendDebugLog("No env overrides, skipping"); + this.sendDebugLog("no env overrides, skipping"); return; } - this.sendDebugLog("Processing env overrides", overrides); + this.sendDebugLog("processing env overrides", overrides); // Override the env with the new values this.env.override(overrides); @@ -916,21 +829,24 @@ export class RunExecution { private async onHeartbeat() { if (!this.runFriendlyId) { - this.sendDebugLog("Heartbeat: missing run ID"); + this.sendDebugLog("heartbeat: missing run ID"); return; } - if (!this.currentSnapshotId) { - this.sendDebugLog("Heartbeat: missing snapshot ID"); + if (!this.snapshotManager) { + this.sendDebugLog("heartbeat: missing snapshot manager"); return; } - this.sendDebugLog("Heartbeat: started"); + this.sendDebugLog("heartbeat"); - const response = await this.httpClient.heartbeatRun(this.runFriendlyId, this.currentSnapshotId); + const response = await this.httpClient.heartbeatRun( + this.runFriendlyId, + this.snapshotManager.snapshotId + ); if (!response.success) { - this.sendDebugLog("Heartbeat: failed", { error: response.error }); + this.sendDebugLog("heartbeat: failed", { error: response.error }); } this.lastHeartbeat = new Date(); @@ -947,7 +863,7 @@ export class RunExecution { properties: { ...properties, runId: this.runFriendlyId, - snapshotId: this.currentSnapshotId, + snapshotId: this.currentSnapshotFriendlyId, executionId: this.id, executionRestoreCount: this.restoreCount, lastHeartbeat: this.lastHeartbeat?.toISOString(), @@ -967,7 +883,7 @@ export class RunExecution { properties: { ...properties, runId: this.runFriendlyId, - snapshotId: this.currentSnapshotId, + snapshotId: this.currentSnapshotFriendlyId, executionId: this.id, executionRestoreCount: this.restoreCount, lastHeartbeat: this.lastHeartbeat?.toISOString(), @@ -975,6 +891,10 @@ export class RunExecution { }); } + private set suspendable(suspendable: boolean) { + this.snapshotManager?.setSuspendable(suspendable); + } + // Ensure we can only set this once private set runFriendlyId(id: string) { if (this._runFriendlyId) { @@ -989,7 +909,7 @@ export class RunExecution { } public get currentSnapshotFriendlyId(): string | undefined { - return this.currentSnapshotId; + return this.snapshotManager?.snapshotId; } public get taskRunEnv(): Record | undefined { @@ -1008,7 +928,7 @@ export class RunExecution { private abortExecution() { if (this.isAborted) { - this.sendDebugLog("Execution already aborted"); + this.sendDebugLog("execution already aborted"); return; } @@ -1024,5 +944,79 @@ export class RunExecution { this.isShuttingDown = true; this.snapshotPoller?.stop(); this.taskRunProcess?.onTaskRunHeartbeat.detach(); + this.snapshotManager?.cleanup(); + } + + private async handleSuspendable(suspendableSnapshot: SnapshotState) { + this.sendDebugLog("handleSuspendable", { suspendableSnapshot }); + + if (!this.snapshotManager) { + this.sendDebugLog("handleSuspendable: missing snapshot manager"); + return; + } + + // Ensure this is the current snapshot + if (suspendableSnapshot.id !== this.currentSnapshotFriendlyId) { + this.sendDebugLog("snapshot changed before cleanup, abort", { + suspendableSnapshot, + currentSnapshotId: this.currentSnapshotFriendlyId, + }); + this.abortExecution(); + return; + } + + // First cleanup the task run process + const [error] = await tryCatch(this.taskRunProcess?.cleanup(false)); + + if (error) { + this.sendDebugLog("failed to cleanup task run process, carrying on", { + suspendableSnapshot, + error: error.message, + }); + } + + // Double check snapshot hasn't changed after cleanup + if (suspendableSnapshot.id !== this.currentSnapshotFriendlyId) { + this.sendDebugLog("snapshot changed after cleanup, abort", { + suspendableSnapshot, + currentSnapshotId: this.currentSnapshotFriendlyId, + }); + this.abortExecution(); + return; + } + + if (!this.runFriendlyId) { + this.sendDebugLog("missing run ID for suspension, abort", { suspendableSnapshot }); + this.abortExecution(); + return; + } + + // Call the suspend API with the current snapshot ID + const suspendResult = await this.httpClient.suspendRun( + this.runFriendlyId, + suspendableSnapshot.id + ); + + if (!suspendResult.success) { + this.sendDebugLog("failed to suspend run, staying alive 🎶", { + suspendableSnapshot, + error: suspendResult.error, + }); + + // This is fine, we'll wait for the next status change + return; + } + + if (!suspendResult.data.ok) { + this.sendDebugLog("checkpoint: failed to suspend run", { + suspendableSnapshot, + error: suspendResult.data.error, + }); + + // This is fine, we'll wait for the next status change + return; + } + + this.sendDebugLog("suspending, any day now 🚬", { suspendableSnapshot }); } } diff --git a/packages/cli-v3/src/entryPoints/managed/poller.ts b/packages/cli-v3/src/entryPoints/managed/poller.ts index 814833846e..148bbbbb15 100644 --- a/packages/cli-v3/src/entryPoints/managed/poller.ts +++ b/packages/cli-v3/src/entryPoints/managed/poller.ts @@ -84,6 +84,7 @@ export class RunExecutionSnapshotPoller { this.poller.resetCurrentInterval(); } + // The snapshot ID is only used as an indicator of when a poller got stuck updateSnapshotId(snapshotFriendlyId: string) { this.snapshotFriendlyId = snapshotFriendlyId; } diff --git a/packages/cli-v3/src/entryPoints/managed/snapshotManager.ts b/packages/cli-v3/src/entryPoints/managed/snapshotManager.ts new file mode 100644 index 0000000000..9d4ca9da5c --- /dev/null +++ b/packages/cli-v3/src/entryPoints/managed/snapshotManager.ts @@ -0,0 +1,280 @@ +import { tryCatch } from "@trigger.dev/core/utils"; +import { RunLogger, SendDebugLogOptions } from "./logger.js"; +import { TaskRunExecutionStatus, type RunExecutionData } from "@trigger.dev/core/v3"; +import { assertExhaustive } from "@trigger.dev/core/utils"; + +export type SnapshotState = { + id: string; + status: TaskRunExecutionStatus; +}; + +type SnapshotHandler = (runData: RunExecutionData) => Promise; +type SuspendableHandler = (suspendableSnapshot: SnapshotState) => Promise; + +type SnapshotManagerOptions = { + runFriendlyId: string; + initialSnapshotId: string; + initialStatus: TaskRunExecutionStatus; + logger: RunLogger; + onSnapshotChange: SnapshotHandler; + onSuspendable: SuspendableHandler; +}; + +type QueuedChange = + | { type: "snapshot"; data: RunExecutionData } + | { type: "suspendable"; value: boolean }; + +type QueuedChangeItem = { + change: QueuedChange; + resolve: () => void; + reject: (error: Error) => void; +}; + +export class SnapshotManager { + private state: SnapshotState; + private runFriendlyId: string; + private logger: RunLogger; + private isSuspendable: boolean = false; + private readonly onSnapshotChange: SnapshotHandler; + private readonly onSuspendable: SuspendableHandler; + + private changeQueue: QueuedChangeItem[] = []; + private isProcessingQueue = false; + + constructor(opts: SnapshotManagerOptions) { + this.runFriendlyId = opts.runFriendlyId; + this.logger = opts.logger; + this.state = { + id: opts.initialSnapshotId, + status: opts.initialStatus, + }; + this.onSnapshotChange = opts.onSnapshotChange; + this.onSuspendable = opts.onSuspendable; + } + + public get snapshotId(): string { + return this.state.id; + } + + public get status(): TaskRunExecutionStatus { + return this.state.status; + } + + public get suspendable(): boolean { + return this.isSuspendable; + } + + public async setSuspendable(suspendable: boolean): Promise { + if (this.isSuspendable === suspendable) { + this.sendDebugLog(`skipping suspendable update, already ${suspendable}`); + return; + } + + this.sendDebugLog(`setting suspendable to ${suspendable}`); + + return this.enqueueSnapshotChange({ type: "suspendable", value: suspendable }); + } + + public updateSnapshot(snapshotId: string, status: TaskRunExecutionStatus) { + // Check if this is an old snapshot + if (snapshotId < this.state.id) { + this.sendDebugLog("skipping update for old snapshot", { + incomingId: snapshotId, + currentId: this.state.id, + }); + return; + } + + this.state = { id: snapshotId, status }; + } + + public async handleSnapshotChange(runData: RunExecutionData): Promise { + if (!this.statusCheck(runData)) { + return; + } + + return this.enqueueSnapshotChange({ type: "snapshot", data: runData }); + } + + private statusCheck(runData: RunExecutionData): boolean { + const { run, snapshot } = runData; + + const statusCheckData = { + incomingId: snapshot.friendlyId, + incomingStatus: snapshot.executionStatus, + currentId: this.state.id, + currentStatus: this.state.status, + }; + + // Ensure run ID matches + if (run.friendlyId !== this.runFriendlyId) { + this.sendDebugLog("skipping update for mismatched run ID", { + statusCheckData, + }); + + return false; + } + + // Skip if this is an old snapshot + if (snapshot.friendlyId < this.state.id) { + this.sendDebugLog("skipping update for old snapshot", { + statusCheckData, + }); + + return false; + } + + // Skip if this is the current snapshot + if (snapshot.friendlyId === this.state.id) { + this.sendDebugLog("skipping update for duplicate snapshot", { + statusCheckData, + }); + + return false; + } + + return true; + } + + private async enqueueSnapshotChange(change: QueuedChange): Promise { + return new Promise((resolve, reject) => { + // For suspendable changes, resolve and remove any pending suspendable changes + // since only the last one matters + if (change.type === "suspendable") { + const pendingSuspendable = this.changeQueue.filter( + (item) => item.change.type === "suspendable" + ); + + // Resolve any pending suspendable changes - they're effectively done since we're superseding them + pendingSuspendable.forEach((item) => item.resolve()); + + // Remove them from the queue + this.changeQueue = this.changeQueue.filter((item) => item.change.type !== "suspendable"); + } + + this.changeQueue.push({ change, resolve, reject }); + + // Sort queue: + // 1. Suspendable changes always go to the back + // 2. Snapshot changes are ordered by ID + this.changeQueue.sort((a, b) => { + if (a.change.type === "suspendable" && b.change.type === "snapshot") { + return 1; // a goes after b + } + if (a.change.type === "snapshot" && b.change.type === "suspendable") { + return -1; // a goes before b + } + if (a.change.type === "snapshot" && b.change.type === "snapshot") { + // sort snapshot changes by creation time, CUIDs are sortable + return a.change.data.snapshot.friendlyId.localeCompare(b.change.data.snapshot.friendlyId); + } + return 0; // both suspendable, maintain insertion order + }); + + // Start processing if not already running + this.processQueue().catch((error) => { + this.sendDebugLog("error processing queue", { error: error.message }); + }); + }); + } + + private async processQueue() { + if (this.isProcessingQueue) { + return; + } + + this.isProcessingQueue = true; + + try { + while (this.changeQueue.length > 0) { + const item = this.changeQueue[0]; + if (!item) { + break; + } + + const [error] = await tryCatch(this.applyChange(item.change)); + + // Remove from queue and resolve/reject promise + this.changeQueue.shift(); + if (error) { + item.reject(error); + } else { + item.resolve(); + } + } + } finally { + this.isProcessingQueue = false; + } + } + + private async applyChange(change: QueuedChange): Promise { + switch (change.type) { + case "snapshot": { + // Double check we should process this snapshot + if (!this.statusCheck(change.data)) { + return; + } + const { snapshot } = change.data; + + this.updateSnapshot(snapshot.friendlyId, snapshot.executionStatus); + + const oldState = { ...this.state }; + + this.sendDebugLog(`status changed to ${snapshot.executionStatus}`, { + oldId: oldState.id, + newId: snapshot.friendlyId, + oldStatus: oldState.status, + newStatus: snapshot.executionStatus, + }); + + // Execute handler + await this.onSnapshotChange(change.data); + + // Check suspendable state after snapshot change + await this.checkSuspendableState(); + break; + } + case "suspendable": { + this.isSuspendable = change.value; + + // Check suspendable state after suspendable change + await this.checkSuspendableState(); + break; + } + default: { + assertExhaustive(change); + } + } + } + + private async checkSuspendableState() { + if ( + this.isSuspendable && + (this.state.status === "EXECUTING_WITH_WAITPOINTS" || + this.state.status === "QUEUED_EXECUTING") + ) { + this.sendDebugLog("run is now suspendable, executing handler"); + await this.onSuspendable(this.state); + } + } + + public cleanup() { + // Clear any pending changes + this.changeQueue = []; + } + + protected sendDebugLog(message: string, properties?: SendDebugLogOptions["properties"]) { + this.logger.sendDebugLog({ + runId: this.runFriendlyId, + message: `[snapshot] ${message}`, + properties: { + ...properties, + snapshotId: this.state.id, + status: this.state.status, + suspendable: this.isSuspendable, + queueLength: this.changeQueue.length, + isProcessingQueue: this.isProcessingQueue, + }, + }); + } +} From 479c30429d7aed7ef42d92d2fff6add759a9b6c0 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:41:58 +0100 Subject: [PATCH 17/62] add terminal link as cli module so we can more easily patch it --- packages/cli-v3/package.json | 3 + packages/cli-v3/src/utilities/cliOutput.ts | 2 +- .../src/utilities/supportsHyperlinks.ts | 146 ++++++++++ packages/cli-v3/src/utilities/terminalLink.ts | 84 ++++++ pnpm-lock.yaml | 265 ++++++++++-------- 5 files changed, 376 insertions(+), 124 deletions(-) create mode 100644 packages/cli-v3/src/utilities/supportsHyperlinks.ts create mode 100644 packages/cli-v3/src/utilities/terminalLink.ts diff --git a/packages/cli-v3/package.json b/packages/cli-v3/package.json index 3ddca96343..cc5c216dec 100644 --- a/packages/cli-v3/package.json +++ b/packages/cli-v3/package.json @@ -91,6 +91,7 @@ "@opentelemetry/semantic-conventions": "1.25.1", "@trigger.dev/build": "workspace:4.0.0-v4-beta.11", "@trigger.dev/core": "workspace:4.0.0-v4-beta.11", + "ansi-escapes": "^7.0.0", "c12": "^1.11.1", "chalk": "^5.2.0", "chokidar": "^3.6.0", @@ -103,6 +104,7 @@ "evt": "^2.4.13", "fast-npm-meta": "^0.2.2", "gradient-string": "^2.0.2", + "has-flag": "^5.0.1", "import-in-the-middle": "1.11.0", "import-meta-resolve": "^4.1.0", "jsonc-parser": "3.2.1", @@ -123,6 +125,7 @@ "socket.io-client": "4.7.5", "source-map-support": "0.5.21", "std-env": "^3.7.0", + "supports-color": "^10.0.0", "terminal-link": "^3.0.0", "tiny-invariant": "^1.2.0", "tinyexec": "^0.3.1", diff --git a/packages/cli-v3/src/utilities/cliOutput.ts b/packages/cli-v3/src/utilities/cliOutput.ts index 48f7316d87..8b499c87c9 100644 --- a/packages/cli-v3/src/utilities/cliOutput.ts +++ b/packages/cli-v3/src/utilities/cliOutput.ts @@ -1,6 +1,6 @@ import { log } from "@clack/prompts"; import chalk from "chalk"; -import terminalLink, { Options as TerminalLinkOptions } from "terminal-link"; +import { terminalLink, TerminalLinkOptions } from "./terminalLink.js"; import { hasTTY } from "std-env"; export const isInteractive = hasTTY; diff --git a/packages/cli-v3/src/utilities/supportsHyperlinks.ts b/packages/cli-v3/src/utilities/supportsHyperlinks.ts new file mode 100644 index 0000000000..891faeed20 --- /dev/null +++ b/packages/cli-v3/src/utilities/supportsHyperlinks.ts @@ -0,0 +1,146 @@ +import { createSupportsColor } from "supports-color"; +import hasFlag from "has-flag"; + +function parseVersion(versionString = ""): { major: number; minor: number; patch: number } { + if (/^\d{3,4}$/.test(versionString)) { + // Env var doesn't always use dots. example: 4601 => 46.1.0 + const match = /(\d{1,2})(\d{2})/.exec(versionString) ?? []; + return { + major: 0, + minor: Number.parseInt(match[1] ?? "0", 10), + patch: Number.parseInt(match[2] ?? "0", 10), + }; + } + + const versions = (versionString ?? "").split(".").map((n) => Number.parseInt(n, 10)); + return { + major: versions[0] ?? 0, + minor: versions[1] ?? 0, + patch: versions[2] ?? 0, + }; +} + +/** + Creates a supports hyperlinks check for a given stream. + + @param stream - Optional stream to check for hyperlink support. + @returns boolean indicating whether hyperlinks are supported. +*/ +export function createSupportsHyperlinks(stream: NodeJS.WriteStream): boolean { + const { + CI, + FORCE_HYPERLINK, + NETLIFY, + TEAMCITY_VERSION, + TERM_PROGRAM, + TERM_PROGRAM_VERSION, + VTE_VERSION, + TERM, + } = process.env; + + if (FORCE_HYPERLINK) { + return !(FORCE_HYPERLINK.length > 0 && Number.parseInt(FORCE_HYPERLINK, 10) === 0); + } + + if ( + hasFlag("no-hyperlink") || + hasFlag("no-hyperlinks") || + hasFlag("hyperlink=false") || + hasFlag("hyperlink=never") + ) { + return false; + } + + if (hasFlag("hyperlink=true") || hasFlag("hyperlink=always")) { + return true; + } + + // Netlify does not run a TTY, it does not need `supportsColor` check + if (NETLIFY) { + return true; + } + + // If they specify no colors, they probably don't want hyperlinks. + if (!createSupportsColor(stream)) { + return false; + } + + if (stream && !stream.isTTY) { + return false; + } + + // Windows Terminal + if ("WT_SESSION" in process.env) { + return true; + } + + if (process.platform === "win32") { + return false; + } + + if (CI) { + return false; + } + + if (TEAMCITY_VERSION) { + return false; + } + + if (TERM_PROGRAM) { + const version = parseVersion(TERM_PROGRAM_VERSION); + + switch (TERM_PROGRAM) { + case "iTerm.app": { + if (version.major === 3) { + return version.minor >= 1; + } + + return version.major > 3; + } + + case "WezTerm": { + return version.major >= 20_200_620; + } + + case "vscode": { + // eslint-disable-next-line no-mixed-operators + return version.major > 1 || (version.major === 1 && version.minor >= 72); + } + + case "ghostty": { + return true; + } + // No default + } + } + + if (VTE_VERSION) { + // 0.50.0 was supposed to support hyperlinks, but throws a segfault + if (VTE_VERSION === "0.50.0") { + return false; + } + + const version = parseVersion(VTE_VERSION); + return version.major > 0 || version.minor >= 50; + } + + switch (TERM) { + case "alacritty": { + // Support added in v0.11 (2022-10-13) + return true; + } + // No default + } + + return false; +} + +/** Object containing hyperlink support status for stdout and stderr. */ +const supportsHyperlinks = { + /** Whether stdout supports hyperlinks. */ + stdout: createSupportsHyperlinks(process.stdout), + /** Whether stderr supports hyperlinks. */ + stderr: createSupportsHyperlinks(process.stderr), +}; + +export default supportsHyperlinks; diff --git a/packages/cli-v3/src/utilities/terminalLink.ts b/packages/cli-v3/src/utilities/terminalLink.ts new file mode 100644 index 0000000000..e887a5f481 --- /dev/null +++ b/packages/cli-v3/src/utilities/terminalLink.ts @@ -0,0 +1,84 @@ +import ansiEscapes from "ansi-escapes"; +import supportsHyperlinks from "./supportsHyperlinks.js"; + +export type TerminalLinkOptions = { + /** + Override the default fallback. If false, the fallback will be disabled. + @default `${text} (${url})` + */ + readonly fallback?: ((text: string, url: string) => string) | boolean; +}; + +/** + Create a clickable link in the terminal's stdout. + + [Supported terminals.](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) + For unsupported terminals, the link will be printed in parens after the text: `My website (https://sindresorhus.com)`, + unless the fallback is disabled by setting the `fallback` option to `false`. + + @param text - Text to linkify. + @param url - URL to link to. + + @example + ``` + import terminalLink from 'terminal-link'; + + const link = terminalLink('My Website', 'https://sindresorhus.com'); + console.log(link); + ``` +*/ +function terminalLink( + text: string, + url: string, + { target = "stdout", ...options }: { target?: "stdout" | "stderr" } & TerminalLinkOptions = {} +) { + if (!supportsHyperlinks[target]) { + // If the fallback has been explicitly disabled, don't modify the text itself. + if (options.fallback === false) { + return text; + } + + return typeof options.fallback === "function" + ? options.fallback(text, url) + : `${text} (\u200B${url}\u200B)`; + } + + return ansiEscapes.link(text, url); +} +/** + Check whether the terminal supports links. + + Prefer just using the default fallback or the `fallback` option whenever possible. +*/ +terminalLink.isSupported = supportsHyperlinks.stdout; +terminalLink.stderr = terminalLinkStderr; + +/** + Create a clickable link in the terminal's stderr. + + [Supported terminals.](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) + For unsupported terminals, the link will be printed in parens after the text: `My website (https://sindresorhus.com)`. + + @param text - Text to linkify. + @param url - URL to link to. + + @example + ``` + import terminalLink from 'terminal-link'; + + const link = terminalLink.stderr('My Website', 'https://sindresorhus.com'); + console.error(link); + ``` +*/ +function terminalLinkStderr(text: string, url: string, options: TerminalLinkOptions = {}) { + return terminalLink(text, url, { target: "stderr", ...options }); +} + +/** + Check whether the terminal's stderr supports links. + + Prefer just using the default fallback or the `fallback` option whenever possible. +*/ +terminalLinkStderr.isSupported = supportsHyperlinks.stderr; + +export { terminalLink }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8c8d014e7..5ff20d804c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,7 +284,7 @@ importers: version: 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': specifier: 0.52.1 - version: 0.52.1(@opentelemetry/api@1.9.0) + version: 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) '@opentelemetry/instrumentation-express': specifier: ^0.36.1 version: 0.36.1(@opentelemetry/api@1.9.0) @@ -299,7 +299,7 @@ importers: version: 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': specifier: 0.52.1 - version: 0.52.1(@opentelemetry/api@1.9.0) + version: 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) '@opentelemetry/sdk-trace-base': specifier: 1.25.1 version: 1.25.1(@opentelemetry/api@1.9.0) @@ -1118,7 +1118,7 @@ importers: version: 0.0.1-cli.2.80.0 '@modelcontextprotocol/sdk': specifier: ^1.6.1 - version: 1.6.1 + version: 1.6.1(supports-color@10.0.0) '@opentelemetry/api': specifier: 1.9.0 version: 1.9.0 @@ -1133,10 +1133,10 @@ importers: version: 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': specifier: 0.52.1 - version: 0.52.1(@opentelemetry/api@1.9.0) + version: 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) '@opentelemetry/instrumentation-fetch': specifier: 0.52.1 - version: 0.52.1(@opentelemetry/api@1.9.0) + version: 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) '@opentelemetry/resources': specifier: 1.25.1 version: 1.25.1(@opentelemetry/api@1.9.0) @@ -1145,7 +1145,7 @@ importers: version: 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': specifier: 0.52.1 - version: 0.52.1(@opentelemetry/api@1.9.0) + version: 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) '@opentelemetry/sdk-trace-base': specifier: 1.25.1 version: 1.25.1(@opentelemetry/api@1.9.0) @@ -1161,6 +1161,9 @@ importers: '@trigger.dev/core': specifier: workspace:4.0.0-v4-beta.11 version: link:../core + ansi-escapes: + specifier: ^7.0.0 + version: 7.0.0 c12: specifier: ^1.11.1 version: 1.11.1(magicast@0.3.4) @@ -1197,6 +1200,9 @@ importers: gradient-string: specifier: ^2.0.2 version: 2.0.2 + has-flag: + specifier: ^5.0.1 + version: 5.0.1 import-in-the-middle: specifier: 1.11.0 version: 1.11.0 @@ -1250,13 +1256,16 @@ importers: version: 4.1.0 socket.io-client: specifier: 4.7.5 - version: 4.7.5 + version: 4.7.5(supports-color@10.0.0) source-map-support: specifier: 0.5.21 version: 0.5.21 std-env: specifier: ^3.7.0 version: 3.7.0 + supports-color: + specifier: ^10.0.0 + version: 10.0.0 terminal-link: specifier: ^3.0.0 version: 3.0.0 @@ -1338,7 +1347,7 @@ importers: version: 4.17.0 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@20.14.14) + version: 2.0.5(@types/node@20.14.14)(supports-color@10.0.0) packages/core: dependencies: @@ -1371,7 +1380,7 @@ importers: version: 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': specifier: 0.52.1 - version: 0.52.1(@opentelemetry/api@1.9.0) + version: 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) '@opentelemetry/resources': specifier: 1.25.1 version: 1.25.1(@opentelemetry/api@1.9.0) @@ -1380,7 +1389,7 @@ importers: version: 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': specifier: 0.52.1 - version: 0.52.1(@opentelemetry/api@1.9.0) + version: 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) '@opentelemetry/sdk-trace-base': specifier: 1.25.1 version: 1.25.1(@opentelemetry/api@1.9.0) @@ -1419,7 +1428,7 @@ importers: version: 4.7.4 socket.io-client: specifier: 4.7.5 - version: 4.7.5 + version: 4.7.5(supports-color@10.0.0) std-env: specifier: ^3.8.1 version: 3.8.1 @@ -3767,7 +3776,7 @@ packages: '@babel/traverse': 7.24.7 '@babel/types': 7.24.0 convert-source-map: 1.9.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -3789,7 +3798,7 @@ packages: '@babel/traverse': 7.24.7 '@babel/types': 7.26.8 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -3813,7 +3822,7 @@ packages: '@babel/types': 7.26.8 '@types/gensync': 1.0.4 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -3978,7 +3987,7 @@ packages: '@babel/core': 7.22.17 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-plugin-utils': 7.24.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) lodash.debounce: 4.0.8 resolve: 1.22.8 semver: 6.3.1 @@ -3994,7 +4003,7 @@ packages: '@babel/core': 7.22.17 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-plugin-utils': 7.24.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -5434,7 +5443,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.24.7 '@babel/types': 7.24.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5452,7 +5461,7 @@ packages: '@babel/helper-split-export-declaration': 7.24.7 '@babel/parser': 7.26.8 '@babel/types': 7.26.8 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5466,7 +5475,7 @@ packages: '@babel/parser': 7.26.8 '@babel/template': 7.25.0 '@babel/types': 7.26.8 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5481,7 +5490,7 @@ packages: '@babel/parser': 7.26.8 '@babel/template': 7.26.8 '@babel/types': 7.26.8 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7903,7 +7912,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) espree: 9.6.0 globals: 13.19.0 ignore: 5.2.4 @@ -7920,7 +7929,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) espree: 9.6.1 globals: 13.19.0 ignore: 5.2.4 @@ -8190,7 +8199,7 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -8200,7 +8209,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -8869,14 +8878,14 @@ packages: - supports-color dev: true - /@modelcontextprotocol/sdk@1.6.1: + /@modelcontextprotocol/sdk@1.6.1(supports-color@10.0.0): resolution: {integrity: sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA==} engines: {node: '>=18'} dependencies: content-type: 1.0.5 cors: 2.8.5 eventsource: 3.0.5 - express: 5.0.1 + express: 5.0.1(supports-color@10.0.0) express-rate-limit: 7.5.0(express@5.0.1) pkce-challenge: 4.1.0 raw-body: 3.0.0 @@ -9679,7 +9688,7 @@ packages: - supports-color dev: true - /@opentelemetry/instrumentation-fetch@0.52.1(@opentelemetry/api@1.9.0): + /@opentelemetry/instrumentation-fetch@0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0): resolution: {integrity: sha512-EJDQXdv1ZGyBifox+8BK+hP0tg29abNPdScE+lW77bUVrThD5vn2dOo+blAS3Z8Od+eqTUTDzXVDIFjGgTK01w==} engines: {node: '>=14'} peerDependencies: @@ -9687,7 +9696,7 @@ packages: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) '@opentelemetry/sdk-trace-web': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: @@ -9717,7 +9726,7 @@ packages: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) '@opentelemetry/semantic-conventions': 1.25.1 semver: 7.6.3 transitivePeerDependencies: @@ -9747,7 +9756,7 @@ packages: '@opentelemetry/api-logs': 0.49.1 '@types/shimmer': 1.0.2 import-in-the-middle: 1.7.1 - require-in-the-middle: 7.1.1 + require-in-the-middle: 7.1.1(supports-color@10.0.0) semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: @@ -9764,7 +9773,7 @@ packages: '@opentelemetry/api-logs': 0.49.1 '@types/shimmer': 1.0.2 import-in-the-middle: 1.7.1 - require-in-the-middle: 7.1.1 + require-in-the-middle: 7.1.1(supports-color@10.0.0) semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: @@ -9781,7 +9790,7 @@ packages: '@opentelemetry/api-logs': 0.49.1 '@types/shimmer': 1.0.2 import-in-the-middle: 1.7.1 - require-in-the-middle: 7.1.1 + require-in-the-middle: 7.1.1(supports-color@10.0.0) semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: @@ -9798,7 +9807,7 @@ packages: '@opentelemetry/api-logs': 0.51.1 '@types/shimmer': 1.0.2 import-in-the-middle: 1.7.4 - require-in-the-middle: 7.1.1 + require-in-the-middle: 7.1.1(supports-color@10.0.0) semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: @@ -9815,14 +9824,14 @@ packages: '@opentelemetry/api-logs': 0.52.1 '@types/shimmer': 1.0.2 import-in-the-middle: 1.11.0 - require-in-the-middle: 7.1.1 + require-in-the-middle: 7.1.1(supports-color@10.0.0) semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0): + /@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0): resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} engines: {node: '>=14'} peerDependencies: @@ -9832,7 +9841,7 @@ packages: '@opentelemetry/api-logs': 0.52.1 '@types/shimmer': 1.0.2 import-in-the-middle: 1.11.0 - require-in-the-middle: 7.1.1 + require-in-the-middle: 7.1.1(supports-color@10.0.0) semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: @@ -10180,7 +10189,7 @@ packages: - supports-color dev: true - /@opentelemetry/sdk-node@0.52.1(@opentelemetry/api@1.9.0): + /@opentelemetry/sdk-node@0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0): resolution: {integrity: sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==} engines: {node: '>=14'} peerDependencies: @@ -10193,7 +10202,7 @@ packages: '@opentelemetry/exporter-trace-otlp-http': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-trace-otlp-proto': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-zipkin': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-logs': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.0) @@ -10618,7 +10627,7 @@ packages: engines: {node: '>=18'} hasBin: true dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.4.0 @@ -18662,7 +18671,7 @@ packages: '@typescript-eslint/scope-manager': 5.59.6 '@typescript-eslint/types': 5.59.6 '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) eslint: 8.31.0 typescript: 5.5.4 transitivePeerDependencies: @@ -18689,7 +18698,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) eslint: 8.31.0 tsutils: 3.21.0(typescript@5.5.4) typescript: 5.5.4 @@ -18713,7 +18722,7 @@ packages: dependencies: '@typescript-eslint/types': 5.59.6 '@typescript-eslint/visitor-keys': 5.59.6 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 @@ -18941,7 +18950,7 @@ packages: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -19511,7 +19520,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) transitivePeerDependencies: - supports-color dev: false @@ -19520,7 +19529,7 @@ packages: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) transitivePeerDependencies: - supports-color @@ -19528,7 +19537,7 @@ packages: resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} engines: {node: '>= 14'} dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) transitivePeerDependencies: - supports-color @@ -19774,7 +19783,6 @@ packages: engines: {node: '>=18'} dependencies: environment: 1.1.0 - dev: true /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -20470,13 +20478,13 @@ packages: transitivePeerDependencies: - supports-color - /body-parser@2.1.0: + /body-parser@2.1.0(supports-color@10.0.0): resolution: {integrity: sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==} engines: {node: '>=18'} dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) http-errors: 2.0.0 iconv-lite: 0.5.2 on-finished: 2.4.1 @@ -20793,7 +20801,7 @@ packages: /capnp-ts@0.7.0: resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -21913,7 +21921,7 @@ packages: dependencies: ms: 2.1.2 - /debug@4.3.6: + /debug@4.3.6(supports-color@10.0.0): resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} engines: {node: '>=6.0'} peerDependencies: @@ -21923,8 +21931,9 @@ packages: optional: true dependencies: ms: 2.1.2 + supports-color: 10.0.0 - /debug@4.3.7: + /debug@4.3.7(supports-color@10.0.0): resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} peerDependencies: @@ -21934,8 +21943,9 @@ packages: optional: true dependencies: ms: 2.1.3 + supports-color: 10.0.0 - /debug@4.4.0: + /debug@4.4.0(supports-color@10.0.0): resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} peerDependencies: @@ -21945,6 +21955,7 @@ packages: optional: true dependencies: ms: 2.1.3 + supports-color: 10.0.0 /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -22204,7 +22215,7 @@ packages: resolution: {integrity: sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==} engines: {node: '>= 8.0'} dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) readable-stream: 3.6.0 split-ca: 1.0.1 ssh2: 1.16.0 @@ -22216,7 +22227,7 @@ packages: resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==} engines: {node: '>= 8.0'} dependencies: - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) readable-stream: 3.6.0 split-ca: 1.0.1 ssh2: 1.16.0 @@ -22462,11 +22473,11 @@ packages: dependencies: once: 1.4.0 - /engine.io-client@6.5.3: + /engine.io-client@6.5.3(supports-color@10.0.0): resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} dependencies: '@socket.io/component-emitter': 3.1.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) engine.io-parser: 5.2.2(patch_hash=e6nctogrhpxoivwiwy37ersfu4) ws: 8.11.0 xmlhttprequest-ssl: 2.0.0 @@ -22533,7 +22544,6 @@ packages: /environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - dev: true /err-code@2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} @@ -23263,7 +23273,7 @@ packages: eslint: '*' eslint-plugin-import: '*' dependencies: - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) enhanced-resolve: 5.15.0 eslint: 8.31.0 eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) @@ -23658,7 +23668,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -23918,7 +23928,7 @@ packages: peerDependencies: express: ^4.11 || 5 || ^5.0.0-beta.1 dependencies: - express: 5.0.1 + express: 5.0.1(supports-color@10.0.0) dev: false /express@4.18.2: @@ -23959,22 +23969,22 @@ packages: transitivePeerDependencies: - supports-color - /express@5.0.1: + /express@5.0.1(supports-color@10.0.0): resolution: {integrity: sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==} engines: {node: '>= 18'} dependencies: accepts: 2.0.0 - body-parser: 2.1.0 + body-parser: 2.1.0(supports-color@10.0.0) content-disposition: 1.0.0 content-type: 1.0.5 cookie: 0.7.1 cookie-signature: 1.2.2 - debug: 4.3.6 + debug: 4.3.6(supports-color@10.0.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.0 + finalhandler: 2.1.0(supports-color@10.0.0) fresh: 2.0.0 http-errors: 2.0.0 merge-descriptors: 2.0.0 @@ -23988,8 +23998,8 @@ packages: range-parser: 1.2.1 router: 2.1.0 safe-buffer: 5.2.1 - send: 1.1.0 - serve-static: 2.1.0 + send: 1.1.0(supports-color@10.0.0) + serve-static: 2.1.0(supports-color@10.0.0) setprototypeof: 1.2.0 statuses: 2.0.1 type-is: 2.0.0 @@ -24020,7 +24030,7 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -24214,11 +24224,11 @@ packages: transitivePeerDependencies: - supports-color - /finalhandler@2.1.0: + /finalhandler@2.1.0(supports-color@10.0.0): resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -24680,7 +24690,7 @@ packages: dependencies: basic-ftp: 5.0.3 data-uri-to-buffer: 5.0.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) fs-extra: 8.1.0 transitivePeerDependencies: - supports-color @@ -24912,7 +24922,7 @@ packages: '@types/node': 20.14.14 '@types/semver': 7.5.1 chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) interpret: 3.1.1 semver: 7.6.3 tslib: 2.6.2 @@ -25001,6 +25011,11 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + /has-flag@5.0.1: + resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} + engines: {node: '>=12'} + dev: false + /has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} dependencies: @@ -25198,7 +25213,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) transitivePeerDependencies: - supports-color @@ -25207,7 +25222,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) transitivePeerDependencies: - supports-color dev: false @@ -25226,7 +25241,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) transitivePeerDependencies: - supports-color dev: false @@ -25236,7 +25251,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) transitivePeerDependencies: - supports-color @@ -25245,7 +25260,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) transitivePeerDependencies: - supports-color dev: false @@ -25475,7 +25490,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -25903,7 +25918,7 @@ packages: engines: {node: '>=10'} dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -27617,7 +27632,7 @@ packages: resolution: {integrity: sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==} dependencies: '@types/debug': 4.1.12 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.0.6 micromark-factory-space: 1.0.0 @@ -27641,7 +27656,7 @@ packages: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} dependencies: '@types/debug': 4.1.12 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -29099,7 +29114,7 @@ packages: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) get-uri: 6.0.1 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 @@ -30229,7 +30244,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 lru-cache: 7.18.3 @@ -30286,7 +30301,7 @@ packages: dependencies: '@puppeteer/browsers': 2.4.0 chromium-bidi: 0.6.5(devtools-protocol@0.0.1342118) - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) devtools-protocol: 0.0.1342118 typed-query-selector: 2.12.0 ws: 8.18.0(bufferutil@4.0.9) @@ -31391,7 +31406,7 @@ packages: remix-auth: ^3.6.0 dependencies: '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) remix-auth: 3.6.0(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0) transitivePeerDependencies: - supports-color @@ -31507,11 +31522,11 @@ packages: engines: {node: '>=0.10.0'} dev: true - /require-in-the-middle@7.1.1: + /require-in-the-middle@7.1.1(supports-color@10.0.0): resolution: {integrity: sha512-OScOjQjrrjhAdFpQmnkE/qbIBGCRFhQB/YaJhcC3CPOlmhe7llnW46Ac1J5+EjcNXOTnDdpF96Erw/yedsGksQ==} engines: {node: '>=8.6.0'} dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) module-details-from-path: 1.0.3 resolve: 1.22.8 transitivePeerDependencies: @@ -31913,11 +31928,11 @@ packages: transitivePeerDependencies: - supports-color - /send@1.1.0: + /send@1.1.0(supports-color@10.0.0): resolution: {integrity: sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==} engines: {node: '>= 18'} dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) destroy: 1.2.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -31949,14 +31964,14 @@ packages: transitivePeerDependencies: - supports-color - /serve-static@2.1.0: + /serve-static@2.1.0(supports-color@10.0.0): resolution: {integrity: sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==} engines: {node: '>= 18'} dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.1.0 + send: 1.1.0(supports-color@10.0.0) transitivePeerDependencies: - supports-color dev: false @@ -32227,7 +32242,7 @@ packages: /socket.io-adapter@2.5.4: resolution: {integrity: sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==} dependencies: - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) ws: 8.11.0 transitivePeerDependencies: - bufferutil @@ -32240,35 +32255,35 @@ packages: engines: {node: '>=10.0.0'} dependencies: '@socket.io/component-emitter': 3.1.0 - debug: 4.3.7 - engine.io-client: 6.5.3 - socket.io-parser: 4.2.4 + debug: 4.3.7(supports-color@10.0.0) + engine.io-client: 6.5.3(supports-color@10.0.0) + socket.io-parser: 4.2.4(supports-color@10.0.0) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate dev: false - /socket.io-client@4.7.5: + /socket.io-client@4.7.5(supports-color@10.0.0): resolution: {integrity: sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==} engines: {node: '>=10.0.0'} dependencies: '@socket.io/component-emitter': 3.1.0 - debug: 4.3.7 - engine.io-client: 6.5.3 - socket.io-parser: 4.2.4 + debug: 4.3.7(supports-color@10.0.0) + engine.io-client: 6.5.3(supports-color@10.0.0) + socket.io-parser: 4.2.4(supports-color@10.0.0) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate dev: false - /socket.io-parser@4.2.4: + /socket.io-parser@4.2.4(supports-color@10.0.0): resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} engines: {node: '>=10.0.0'} dependencies: '@socket.io/component-emitter': 3.1.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) transitivePeerDependencies: - supports-color dev: false @@ -32280,10 +32295,10 @@ packages: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) engine.io: 6.5.4 socket.io-adapter: 2.5.4 - socket.io-parser: 4.2.4 + socket.io-parser: 4.2.4(supports-color@10.0.0) transitivePeerDependencies: - bufferutil - supports-color @@ -32297,10 +32312,10 @@ packages: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) engine.io: 6.5.4 socket.io-adapter: 2.5.4 - socket.io-parser: 4.2.4 + socket.io-parser: 4.2.4(supports-color@10.0.0) transitivePeerDependencies: - bufferutil - supports-color @@ -32314,10 +32329,10 @@ packages: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) engine.io: 6.5.4 socket.io-adapter: 2.5.4 - socket.io-parser: 4.2.4 + socket.io-parser: 4.2.4(supports-color@10.0.0) transitivePeerDependencies: - bufferutil - supports-color @@ -32329,7 +32344,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -32882,7 +32897,7 @@ packages: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 3.5.1 @@ -32915,6 +32930,10 @@ packages: - supports-color dev: true + /supports-color@10.0.0: + resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==} + engines: {node: '>=18'} + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -33393,7 +33412,7 @@ packages: archiver: 7.0.1 async-lock: 1.4.1 byline: 5.0.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) docker-compose: 0.24.8 dockerode: 3.3.5 get-port: 5.1.1 @@ -33902,7 +33921,7 @@ packages: cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) esbuild: 0.25.1 joycon: 3.1.1 picocolors: 1.1.1 @@ -34914,7 +34933,7 @@ packages: hasBin: true dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) mlly: 1.7.1 pathe: 1.1.2 picocolors: 1.1.1 @@ -34938,7 +34957,7 @@ packages: hasBin: true dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) pathe: 1.1.2 picocolors: 1.1.1 vite: 5.2.7(@types/node@20.14.14) @@ -34959,7 +34978,7 @@ packages: hasBin: true dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) pathe: 1.1.2 picocolors: 1.1.1 vite: 5.2.7(@types/node@20.14.14) @@ -34974,13 +34993,13 @@ packages: - terser dev: true - /vite-node@2.0.5(@types/node@20.14.14): + /vite-node@2.0.5(@types/node@20.14.14)(supports-color@10.0.0): resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) pathe: 1.1.2 tinyrainbow: 1.2.0 vite: 5.2.7(@types/node@20.14.14) @@ -35001,7 +35020,7 @@ packages: hasBin: true dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) es-module-lexer: 1.6.0 pathe: 2.0.3 vite: 5.2.7(@types/node@20.14.14) @@ -35019,7 +35038,7 @@ packages: /vite-tsconfig-paths@4.0.5(typescript@5.5.4): resolution: {integrity: sha512-/L/eHwySFYjwxoYt1WRJniuK/jPv+WGwgRGBYx3leciR5wBeqntQpUE6Js6+TJemChc+ter7fDBKieyEWDx4yQ==} dependencies: - debug: 4.3.7 + debug: 4.3.7(supports-color@10.0.0) globrex: 0.1.2 tsconfck: 2.1.2(typescript@5.5.4) transitivePeerDependencies: @@ -35278,7 +35297,7 @@ packages: '@vitest/utils': 1.6.0 acorn-walk: 8.3.2 chai: 4.4.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) execa: 8.0.1 local-pkg: 0.5.0 magic-string: 0.30.17 @@ -35301,7 +35320,7 @@ packages: - terser dev: true - /vitest@2.0.5(@types/node@20.14.14): + /vitest@2.0.5(@types/node@20.14.14)(supports-color@10.0.0): resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -35335,7 +35354,7 @@ packages: '@vitest/spy': 2.0.5 '@vitest/utils': 2.0.5 chai: 5.1.1 - debug: 4.3.6 + debug: 4.3.6(supports-color@10.0.0) execa: 8.0.1 magic-string: 0.30.11 pathe: 1.1.2 @@ -35344,7 +35363,7 @@ packages: tinypool: 1.0.1 tinyrainbow: 1.2.0 vite: 5.2.7(@types/node@20.14.14) - vite-node: 2.0.5(@types/node@20.14.14) + vite-node: 2.0.5(@types/node@20.14.14)(supports-color@10.0.0) why-is-node-running: 2.3.0 transitivePeerDependencies: - less @@ -35393,7 +35412,7 @@ packages: '@vitest/spy': 3.0.8 '@vitest/utils': 3.0.8 chai: 5.2.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) expect-type: 1.2.0 magic-string: 0.30.17 pathe: 2.0.3 From a053114037777935242365b7029cb26021d0e213 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:43:23 +0100 Subject: [PATCH 18/62] apply cursor patch --- packages/cli-v3/src/utilities/supportsHyperlinks.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/cli-v3/src/utilities/supportsHyperlinks.ts b/packages/cli-v3/src/utilities/supportsHyperlinks.ts index 891faeed20..e6fc95ef88 100644 --- a/packages/cli-v3/src/utilities/supportsHyperlinks.ts +++ b/packages/cli-v3/src/utilities/supportsHyperlinks.ts @@ -29,6 +29,7 @@ function parseVersion(versionString = ""): { major: number; minor: number; patch export function createSupportsHyperlinks(stream: NodeJS.WriteStream): boolean { const { CI, + CURSOR_TRACE_ID, FORCE_HYPERLINK, NETLIFY, TEAMCITY_VERSION, @@ -86,6 +87,10 @@ export function createSupportsHyperlinks(stream: NodeJS.WriteStream): boolean { return false; } + if (CURSOR_TRACE_ID) { + return true; + } + if (TERM_PROGRAM) { const version = parseVersion(TERM_PROGRAM_VERSION); From c3333aba9d6bb2bf614d204d93c67694944937e5 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:52:26 +0100 Subject: [PATCH 19/62] add license info --- packages/cli-v3/src/utilities/supportsHyperlinks.ts | 9 +++++++++ packages/cli-v3/src/utilities/terminalLink.ts | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/cli-v3/src/utilities/supportsHyperlinks.ts b/packages/cli-v3/src/utilities/supportsHyperlinks.ts index e6fc95ef88..69c5ee4d31 100644 --- a/packages/cli-v3/src/utilities/supportsHyperlinks.ts +++ b/packages/cli-v3/src/utilities/supportsHyperlinks.ts @@ -1,3 +1,12 @@ +/* + MIT License + Copyright (c) Sindre Sorhus (https://sindresorhus.com) + Copyright (c) James Talmage (https://github.com/jamestalmage) + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + import { createSupportsColor } from "supports-color"; import hasFlag from "has-flag"; diff --git a/packages/cli-v3/src/utilities/terminalLink.ts b/packages/cli-v3/src/utilities/terminalLink.ts index e887a5f481..d12c2a82b0 100644 --- a/packages/cli-v3/src/utilities/terminalLink.ts +++ b/packages/cli-v3/src/utilities/terminalLink.ts @@ -1,3 +1,11 @@ +/* + MIT License + Copyright (c) Sindre Sorhus (https://sindresorhus.com) + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + import ansiEscapes from "ansi-escapes"; import supportsHyperlinks from "./supportsHyperlinks.js"; From 8bc2e339bcff213ba22f65b4f21ff23d2222d503 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:56:00 +0100 Subject: [PATCH 20/62] remove terminal-link package and add deprecation notice --- packages/cli-v3/package.json | 1 - packages/cli-v3/src/utilities/terminalLink.ts | 2 + packages/cli-v3/types.d.ts | 10 --- pnpm-lock.yaml | 83 +++++++++++++++---- 4 files changed, 71 insertions(+), 25 deletions(-) delete mode 100644 packages/cli-v3/types.d.ts diff --git a/packages/cli-v3/package.json b/packages/cli-v3/package.json index cc5c216dec..47c1287719 100644 --- a/packages/cli-v3/package.json +++ b/packages/cli-v3/package.json @@ -126,7 +126,6 @@ "source-map-support": "0.5.21", "std-env": "^3.7.0", "supports-color": "^10.0.0", - "terminal-link": "^3.0.0", "tiny-invariant": "^1.2.0", "tinyexec": "^0.3.1", "tinyglobby": "^0.2.10", diff --git a/packages/cli-v3/src/utilities/terminalLink.ts b/packages/cli-v3/src/utilities/terminalLink.ts index d12c2a82b0..ce60d00b5a 100644 --- a/packages/cli-v3/src/utilities/terminalLink.ts +++ b/packages/cli-v3/src/utilities/terminalLink.ts @@ -34,6 +34,8 @@ export type TerminalLinkOptions = { const link = terminalLink('My Website', 'https://sindresorhus.com'); console.log(link); ``` + + @deprecated The default fallback is broken in some terminals. Please use `cliLink` instead. */ function terminalLink( text: string, diff --git a/packages/cli-v3/types.d.ts b/packages/cli-v3/types.d.ts deleted file mode 100644 index c6d13651b4..0000000000 --- a/packages/cli-v3/types.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare module "terminal-link" { - export interface Options { - fallback?: ((text: string, url: string) => string) | boolean; - } - - /** - * @deprecated The default fallback is broken in some terminals. Please use `cliLink` instead. - */ - export default function terminalLink(text: string, url: string, options?: Options): string; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ff20d804c..17d3d9196d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1266,9 +1266,6 @@ importers: supports-color: specifier: ^10.0.0 version: 10.0.0 - terminal-link: - specifier: ^3.0.0 - version: 3.0.0 tiny-invariant: specifier: ^1.2.0 version: 1.3.1 @@ -1280,7 +1277,7 @@ importers: version: 0.2.10 ws: specifier: ^8.18.0 - version: 8.18.0(bufferutil@4.0.9) + version: 8.18.0 xdg-app-paths: specifier: ^8.3.0 version: 8.3.0 @@ -1338,7 +1335,7 @@ importers: version: 5.0.7 ts-essentials: specifier: 10.0.1 - version: 10.0.1(typescript@5.5.4) + version: 10.0.1 tshy: specifier: ^3.0.2 version: 3.0.2 @@ -1347,7 +1344,7 @@ importers: version: 4.17.0 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@20.14.14)(supports-color@10.0.0) + version: 2.0.5(supports-color@10.0.0) packages/core: dependencies: @@ -6216,7 +6213,7 @@ packages: '@open-draft/deferred-promise': 2.2.0 '@types/ws': 8.5.12 hono: 4.5.11 - ws: 8.18.0(bufferutil@4.0.9) + ws: 8.18.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -8187,7 +8184,7 @@ packages: '@hono/node-server': ^1.11.1 dependencies: '@hono/node-server': 1.12.2(hono@4.5.11) - ws: 8.18.0(bufferutil@4.0.9) + ws: 8.18.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -20595,6 +20592,7 @@ packages: requiresBuild: true dependencies: node-gyp-build: 4.8.4 + dev: false /buildcheck@0.0.6: resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} @@ -28388,6 +28386,7 @@ packages: /node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true + dev: false /node-gyp@10.2.0: resolution: {integrity: sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==} @@ -33701,6 +33700,15 @@ packages: resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} dev: false + /ts-essentials@10.0.1: + resolution: {integrity: sha512-HPH+H2bkkO8FkMDau+hFvv7KYozzned9Zr1Urn7rRPXMF4mZmCKOq+u4AI1AAW+2bofIOXTuSdKo9drQuni2dQ==} + peerDependencies: + typescript: '>=4.5.0' + peerDependenciesMeta: + typescript: + optional: true + dev: true + /ts-essentials@10.0.1(typescript@5.5.4): resolution: {integrity: sha512-HPH+H2bkkO8FkMDau+hFvv7KYozzned9Zr1Urn7rRPXMF4mZmCKOq+u4AI1AAW+2bofIOXTuSdKo9drQuni2dQ==} peerDependencies: @@ -34993,7 +35001,7 @@ packages: - terser dev: true - /vite-node@2.0.5(@types/node@20.14.14)(supports-color@10.0.0): + /vite-node@2.0.5(supports-color@10.0.0): resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -35002,7 +35010,7 @@ packages: debug: 4.4.0(supports-color@10.0.0) pathe: 1.1.2 tinyrainbow: 1.2.0 - vite: 5.2.7(@types/node@20.14.14) + vite: 5.2.7 transitivePeerDependencies: - '@types/node' - less @@ -35116,6 +35124,41 @@ packages: fsevents: 2.3.3 dev: true + /vite@5.2.7: + resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.20.2 + postcss: 8.5.3 + rollup: 4.36.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vite@5.2.7(@types/node@20.14.14): resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -35320,7 +35363,7 @@ packages: - terser dev: true - /vitest@2.0.5(@types/node@20.14.14)(supports-color@10.0.0): + /vitest@2.0.5(supports-color@10.0.0): resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -35346,7 +35389,6 @@ packages: optional: true dependencies: '@ampproject/remapping': 2.3.0 - '@types/node': 20.14.14 '@vitest/expect': 2.0.5 '@vitest/pretty-format': 2.0.5 '@vitest/runner': 2.0.5 @@ -35362,8 +35404,8 @@ packages: tinybench: 2.9.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.2.7(@types/node@20.14.14) - vite-node: 2.0.5(@types/node@20.14.14)(supports-color@10.0.0) + vite: 5.2.7 + vite-node: 2.0.5(supports-color@10.0.0) why-is-node-running: 2.3.0 transitivePeerDependencies: - less @@ -35848,6 +35890,18 @@ packages: optional: true dev: false + /ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + /ws@8.18.0(bufferutil@4.0.9): resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -35861,6 +35915,7 @@ packages: optional: true dependencies: bufferutil: 4.0.9 + dev: false /xdg-app-paths@8.3.0: resolution: {integrity: sha512-mgxlWVZw0TNWHoGmXq+NC3uhCIc55dDpAlDkMQUaIAcQzysb0kxctwv//fvuW61/nAAeUBJMQ8mnZjMmuYwOcQ==} From 784e151743f7237dc39f84338d16cf5f5d2b1bb2 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:57:47 +0100 Subject: [PATCH 21/62] remove old patch --- package.json | 3 +- patches/supports-hyperlinks@2.3.0.patch | 16 ----- pnpm-lock.yaml | 90 ++++--------------------- 3 files changed, 15 insertions(+), 94 deletions(-) delete mode 100644 patches/supports-hyperlinks@2.3.0.patch diff --git a/package.json b/package.json index 20203eb39d..6afb03eb1f 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,7 @@ "engine.io-parser@5.2.2": "patches/engine.io-parser@5.2.2.patch", "graphile-worker@0.16.6": "patches/graphile-worker@0.16.6.patch", "redlock@5.0.0-beta.2": "patches/redlock@5.0.0-beta.2.patch", - "supports-hyperlinks@2.3.0": "patches/supports-hyperlinks@2.3.0.patch", "@kubernetes/client-node@1.0.0": "patches/@kubernetes__client-node@1.0.0.patch" } } -} \ No newline at end of file +} diff --git a/patches/supports-hyperlinks@2.3.0.patch b/patches/supports-hyperlinks@2.3.0.patch deleted file mode 100644 index 9442afbbfd..0000000000 --- a/patches/supports-hyperlinks@2.3.0.patch +++ /dev/null @@ -1,16 +0,0 @@ -diff --git a/index.js b/index.js -index 89f1b2cb86fad204b0493da3b8a3d5ed28937260..945f4cff27ed501fca75e269dfd7172e74c9c955 100644 ---- a/index.js -+++ b/index.js -@@ -62,6 +62,11 @@ function supportsHyperlink(stream) { - return false; - } - -+ // Cursor supports hyperlinks -+ if ("CURSOR_TRACE_ID" in env) { -+ return true; -+ } -+ - if ('TERM_PROGRAM' in env) { - const version = parseVersion(env.TERM_PROGRAM_VERSION); - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17d3d9196d..d7b924a8c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ patchedDependencies: redlock@5.0.0-beta.2: hash: rwyegdki7iserrd7fgjwxkhnlu path: patches/redlock@5.0.0-beta.2.patch - supports-hyperlinks@2.3.0: - hash: xmw2etywyp5w2jf77wkqg4ob3a - path: patches/supports-hyperlinks@2.3.0.patch importers: @@ -1277,7 +1274,7 @@ importers: version: 0.2.10 ws: specifier: ^8.18.0 - version: 8.18.0 + version: 8.18.0(bufferutil@4.0.9) xdg-app-paths: specifier: ^8.3.0 version: 8.3.0 @@ -1335,7 +1332,7 @@ importers: version: 5.0.7 ts-essentials: specifier: 10.0.1 - version: 10.0.1 + version: 10.0.1(typescript@5.5.4) tshy: specifier: ^3.0.2 version: 3.0.2 @@ -1344,7 +1341,7 @@ importers: version: 4.17.0 vitest: specifier: ^2.0.5 - version: 2.0.5(supports-color@10.0.0) + version: 2.0.5(@types/node@20.14.14)(supports-color@10.0.0) packages/core: dependencies: @@ -6213,7 +6210,7 @@ packages: '@open-draft/deferred-promise': 2.2.0 '@types/ws': 8.5.12 hono: 4.5.11 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.9) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -8184,7 +8181,7 @@ packages: '@hono/node-server': ^1.11.1 dependencies: '@hono/node-server': 1.12.2(hono@4.5.11) - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.9) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -20592,7 +20589,6 @@ packages: requiresBuild: true dependencies: node-gyp-build: 4.8.4 - dev: false /buildcheck@0.0.6: resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} @@ -28386,7 +28382,6 @@ packages: /node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - dev: false /node-gyp@10.2.0: resolution: {integrity: sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==} @@ -32958,13 +32953,12 @@ packages: dependencies: has-flag: 4.0.0 - /supports-hyperlinks@2.3.0(patch_hash=xmw2etywyp5w2jf77wkqg4ob3a): + /supports-hyperlinks@2.3.0: resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} engines: {node: '>=8'} dependencies: has-flag: 4.0.0 supports-color: 7.2.0 - patched: true /supports-hyperlinks@3.1.0: resolution: {integrity: sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==} @@ -33321,7 +33315,7 @@ packages: engines: {node: '>=8'} dependencies: ansi-escapes: 4.3.2 - supports-hyperlinks: 2.3.0(patch_hash=xmw2etywyp5w2jf77wkqg4ob3a) + supports-hyperlinks: 2.3.0 dev: true /terminal-link@3.0.0: @@ -33329,7 +33323,7 @@ packages: engines: {node: '>=12'} dependencies: ansi-escapes: 5.0.0 - supports-hyperlinks: 2.3.0(patch_hash=xmw2etywyp5w2jf77wkqg4ob3a) + supports-hyperlinks: 2.3.0 dev: false /terser-webpack-plugin@5.3.7(@swc/core@1.3.101)(esbuild@0.19.11)(webpack@5.88.2): @@ -33700,15 +33694,6 @@ packages: resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} dev: false - /ts-essentials@10.0.1: - resolution: {integrity: sha512-HPH+H2bkkO8FkMDau+hFvv7KYozzned9Zr1Urn7rRPXMF4mZmCKOq+u4AI1AAW+2bofIOXTuSdKo9drQuni2dQ==} - peerDependencies: - typescript: '>=4.5.0' - peerDependenciesMeta: - typescript: - optional: true - dev: true - /ts-essentials@10.0.1(typescript@5.5.4): resolution: {integrity: sha512-HPH+H2bkkO8FkMDau+hFvv7KYozzned9Zr1Urn7rRPXMF4mZmCKOq+u4AI1AAW+2bofIOXTuSdKo9drQuni2dQ==} peerDependencies: @@ -35001,7 +34986,7 @@ packages: - terser dev: true - /vite-node@2.0.5(supports-color@10.0.0): + /vite-node@2.0.5(@types/node@20.14.14)(supports-color@10.0.0): resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -35010,7 +34995,7 @@ packages: debug: 4.4.0(supports-color@10.0.0) pathe: 1.1.2 tinyrainbow: 1.2.0 - vite: 5.2.7 + vite: 5.2.7(@types/node@20.14.14) transitivePeerDependencies: - '@types/node' - less @@ -35124,41 +35109,6 @@ packages: fsevents: 2.3.3 dev: true - /vite@5.2.7: - resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - esbuild: 0.20.2 - postcss: 8.5.3 - rollup: 4.36.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /vite@5.2.7(@types/node@20.14.14): resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -35363,7 +35313,7 @@ packages: - terser dev: true - /vitest@2.0.5(supports-color@10.0.0): + /vitest@2.0.5(@types/node@20.14.14)(supports-color@10.0.0): resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -35389,6 +35339,7 @@ packages: optional: true dependencies: '@ampproject/remapping': 2.3.0 + '@types/node': 20.14.14 '@vitest/expect': 2.0.5 '@vitest/pretty-format': 2.0.5 '@vitest/runner': 2.0.5 @@ -35404,8 +35355,8 @@ packages: tinybench: 2.9.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.2.7 - vite-node: 2.0.5(supports-color@10.0.0) + vite: 5.2.7(@types/node@20.14.14) + vite-node: 2.0.5(@types/node@20.14.14)(supports-color@10.0.0) why-is-node-running: 2.3.0 transitivePeerDependencies: - less @@ -35890,18 +35841,6 @@ packages: optional: true dev: false - /ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - /ws@8.18.0(bufferutil@4.0.9): resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -35915,7 +35854,6 @@ packages: optional: true dependencies: bufferutil: 4.0.9 - dev: false /xdg-app-paths@8.3.0: resolution: {integrity: sha512-mgxlWVZw0TNWHoGmXq+NC3uhCIc55dDpAlDkMQUaIAcQzysb0kxctwv//fvuW61/nAAeUBJMQ8mnZjMmuYwOcQ==} From 641ca671fd2d485cab0e63c70e8cb77f5569e167 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:58:38 +0100 Subject: [PATCH 22/62] remove terminal-link from sdk --- packages/trigger-sdk/package.json | 1 - pnpm-lock.yaml | 24 +----------------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index 4365bc1d3c..42a35519f2 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -58,7 +58,6 @@ "debug": "^4.3.4", "evt": "^2.4.13", "slug": "^6.0.0", - "terminal-link": "^3.0.0", "ulid": "^2.3.0", "uncrypto": "^0.1.3", "uuid": "^9.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7b924a8c2..94bac59ab0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1671,9 +1671,6 @@ importers: slug: specifier: ^6.0.0 version: 6.1.0 - terminal-link: - specifier: ^3.0.0 - version: 3.0.0 ulid: specifier: ^2.3.0 version: 2.3.0 @@ -19765,13 +19762,6 @@ packages: dependencies: type-fest: 0.21.3 - /ansi-escapes@5.0.0: - resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} - engines: {node: '>=12'} - dependencies: - type-fest: 1.4.0 - dev: false - /ansi-escapes@7.0.0: resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} engines: {node: '>=18'} @@ -32959,6 +32949,7 @@ packages: dependencies: has-flag: 4.0.0 supports-color: 7.2.0 + dev: true /supports-hyperlinks@3.1.0: resolution: {integrity: sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==} @@ -33318,14 +33309,6 @@ packages: supports-hyperlinks: 2.3.0 dev: true - /terminal-link@3.0.0: - resolution: {integrity: sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==} - engines: {node: '>=12'} - dependencies: - ansi-escapes: 5.0.0 - supports-hyperlinks: 2.3.0 - dev: false - /terser-webpack-plugin@5.3.7(@swc/core@1.3.101)(esbuild@0.19.11)(webpack@5.88.2): resolution: {integrity: sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==} engines: {node: '>= 10.13.0'} @@ -34119,11 +34102,6 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - /type-fest@1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} - dev: false - /type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} From ccd9e5b4bad540fb6597c53126727f6b6cea084a Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 18:25:19 +0100 Subject: [PATCH 23/62] rename snapshot module --- packages/cli-v3/src/entryPoints/managed/execution.ts | 2 +- .../src/entryPoints/managed/{snapshotManager.ts => snapshot.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/cli-v3/src/entryPoints/managed/{snapshotManager.ts => snapshot.ts} (100%) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 2b13885e07..0396a9a095 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -19,7 +19,7 @@ import { RunExecutionSnapshotPoller } from "./poller.js"; import { assertExhaustive, tryCatch } from "@trigger.dev/core/utils"; import { MetadataClient } from "./overrides.js"; import { randomBytes } from "node:crypto"; -import { SnapshotManager, SnapshotState } from "./snapshotManager.js"; +import { SnapshotManager, SnapshotState } from "./snapshot.js"; class ExecutionAbortError extends Error { constructor(message: string) { diff --git a/packages/cli-v3/src/entryPoints/managed/snapshotManager.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.ts similarity index 100% rename from packages/cli-v3/src/entryPoints/managed/snapshotManager.ts rename to packages/cli-v3/src/entryPoints/managed/snapshot.ts From f62047b84b06bbf65d328279778a7ba0782275f7 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 21:43:07 +0100 Subject: [PATCH 24/62] add cli test tsconfig --- packages/cli-v3/tsconfig.json | 3 +++ packages/cli-v3/tsconfig.src.json | 1 + packages/cli-v3/tsconfig.test.json | 11 +++++++++++ 3 files changed, 15 insertions(+) create mode 100644 packages/cli-v3/tsconfig.test.json diff --git a/packages/cli-v3/tsconfig.json b/packages/cli-v3/tsconfig.json index bef5b58828..d09a1e841e 100644 --- a/packages/cli-v3/tsconfig.json +++ b/packages/cli-v3/tsconfig.json @@ -5,6 +5,9 @@ }, { "path": "./tsconfig.e2e.test.json" + }, + { + "path": "./tsconfig.test.json" } ] } diff --git a/packages/cli-v3/tsconfig.src.json b/packages/cli-v3/tsconfig.src.json index 3ebcb153c9..364e98ce28 100644 --- a/packages/cli-v3/tsconfig.src.json +++ b/packages/cli-v3/tsconfig.src.json @@ -1,6 +1,7 @@ { "extends": "../../.configs/tsconfig.base.json", "include": ["./src/**/*.ts"], + "exclude": ["./src/**/*.test.ts"], "compilerOptions": { "isolatedDeclarations": false, "composite": true, diff --git a/packages/cli-v3/tsconfig.test.json b/packages/cli-v3/tsconfig.test.json new file mode 100644 index 0000000000..52b05f9281 --- /dev/null +++ b/packages/cli-v3/tsconfig.test.json @@ -0,0 +1,11 @@ +{ + "extends": "../../.configs/tsconfig.base.json", + "include": ["./src/**/*.ts"], + "references": [{ "path": "./tsconfig.src.json" }], + "compilerOptions": { + "isolatedDeclarations": false, + "composite": true, + "customConditions": ["@triggerdotdev/source"], + "types": ["vitest/globals"] + } +} From c3dbb8a24959516da2c5ca76ab0eba75e70812a5 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 30 Apr 2025 21:44:25 +0100 Subject: [PATCH 25/62] add run logger base type --- .../src/entryPoints/managed/controller.ts | 4 ++-- .../cli-v3/src/entryPoints/managed/logger.ts | 22 +++++++++++++++++-- .../src/entryPoints/managed/snapshot.ts | 6 +++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/controller.ts b/packages/cli-v3/src/entryPoints/managed/controller.ts index a459ccc7b4..3c7ab6c5e8 100644 --- a/packages/cli-v3/src/entryPoints/managed/controller.ts +++ b/packages/cli-v3/src/entryPoints/managed/controller.ts @@ -8,7 +8,7 @@ import { } from "@trigger.dev/core/v3/workers"; import { io, type Socket } from "socket.io-client"; import { RunnerEnv } from "./env.js"; -import { RunLogger, SendDebugLogOptions } from "./logger.js"; +import { ManagedRunLogger, RunLogger, SendDebugLogOptions } from "./logger.js"; import { EnvObject } from "std-env"; import { RunExecution } from "./execution.js"; import { tryCatch } from "@trigger.dev/core/utils"; @@ -47,7 +47,7 @@ export class ManagedRunController { projectRef: env.TRIGGER_PROJECT_REF, }); - this.logger = new RunLogger({ + this.logger = new ManagedRunLogger({ httpClient: this.httpClient, env, }); diff --git a/packages/cli-v3/src/entryPoints/managed/logger.ts b/packages/cli-v3/src/entryPoints/managed/logger.ts index 56fcaba432..150d740094 100644 --- a/packages/cli-v3/src/entryPoints/managed/logger.ts +++ b/packages/cli-v3/src/entryPoints/managed/logger.ts @@ -14,16 +14,20 @@ export type SendDebugLogOptions = { print?: boolean; }; +export interface RunLogger { + sendDebugLog(options: SendDebugLogOptions): void; +} + export type RunLoggerOptions = { httpClient: WorkloadHttpClient; env: RunnerEnv; }; -export class RunLogger { +export class ManagedRunLogger implements RunLogger { private readonly httpClient: WorkloadHttpClient; private readonly env: RunnerEnv; - constructor(private readonly opts: RunLoggerOptions) { + constructor(opts: RunLoggerOptions) { this.httpClient = opts.httpClient; this.env = opts.env; } @@ -59,3 +63,17 @@ export class RunLogger { }); } } + +export class ConsoleRunLogger implements RunLogger { + private readonly print: boolean; + + constructor(opts: { print?: boolean } = {}) { + this.print = opts.print ?? true; + } + + sendDebugLog({ message, properties }: SendDebugLogOptions): void { + if (this.print) { + console.log("[ConsoleLogger]", message, properties); + } + } +} diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.ts index 9d4ca9da5c..f23590b821 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.ts @@ -75,6 +75,12 @@ export class SnapshotManager { return this.enqueueSnapshotChange({ type: "suspendable", value: suspendable }); } + /** + * Update the snapshot ID and status without invoking any handlers + * + * @param snapshotId - The ID of the snapshot to update to + * @param status - The status to update to + */ public updateSnapshot(snapshotId: string, status: TaskRunExecutionStatus) { // Check if this is an old snapshot if (snapshotId < this.state.id) { From 140e2d79746f3b1d7c7c320b05b1c905ba65fe6d Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 00:49:29 +0100 Subject: [PATCH 26/62] add snapshot manager tests --- packages/cli-v3/package.json | 1 + .../src/entryPoints/managed/execution.ts | 4 +- .../src/entryPoints/managed/snapshot.test.ts | 666 ++++++++++++++++++ .../src/entryPoints/managed/snapshot.ts | 55 +- 4 files changed, 709 insertions(+), 17 deletions(-) create mode 100644 packages/cli-v3/src/entryPoints/managed/snapshot.test.ts diff --git a/packages/cli-v3/package.json b/packages/cli-v3/package.json index 3ddca96343..90e0b0207f 100644 --- a/packages/cli-v3/package.json +++ b/packages/cli-v3/package.json @@ -70,6 +70,7 @@ "typecheck": "tsc -p tsconfig.src.json --noEmit", "build": "tshy && pnpm run update-version", "dev": "tshy --watch", + "test": "vitest", "test:e2e": "vitest --run -c ./e2e/vitest.config.ts", "update-version": "tsx ../../scripts/updateVersion.ts" }, diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 0396a9a095..1095858ce5 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -892,7 +892,9 @@ export class RunExecution { } private set suspendable(suspendable: boolean) { - this.snapshotManager?.setSuspendable(suspendable); + this.snapshotManager?.setSuspendable(suspendable).catch((error) => { + this.sendDebugLog("failed to set suspendable", { error: error.message }); + }); } // Ensure we can only set this once diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts new file mode 100644 index 0000000000..18e93b0497 --- /dev/null +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts @@ -0,0 +1,666 @@ +import { SnapshotManager } from "./snapshot.js"; +import { ConsoleRunLogger } from "./logger.js"; +import { RunExecutionData, TaskRunExecutionStatus, TaskRunStatus } from "@trigger.dev/core/v3"; +import { setTimeout } from "timers/promises"; + +describe("SnapshotManager", () => { + const mockLogger = new ConsoleRunLogger(); + const mockSnapshotHandler = vi.fn(); + const mockSuspendableHandler = vi.fn(); + + let manager: SnapshotManager; + + beforeEach(() => { + vi.clearAllMocks(); + manager = new SnapshotManager({ + runFriendlyId: "test-run-1", + initialSnapshotId: "snapshot-1", + initialStatus: "PENDING_EXECUTING", + logger: mockLogger, + onSnapshotChange: mockSnapshotHandler, + onSuspendable: mockSuspendableHandler, + }); + }); + + it("should initialize with correct initial values", () => { + expect(manager.snapshotId).toBe("snapshot-1"); + expect(manager.status).toBe("PENDING_EXECUTING"); + expect(manager.suspendable).toBe(false); + }); + + it("should update snapshot when newer snapshot ID is provided", () => { + manager.updateSnapshot("snapshot-2", "EXECUTING"); + expect(manager.snapshotId).toBe("snapshot-2"); + expect(manager.status).toBe("EXECUTING"); + }); + + it("should not update snapshot when older snapshot ID is provided", () => { + manager.updateSnapshot("snapshot-2", "EXECUTING"); + manager.updateSnapshot("snapshot-1", "FINISHED"); + expect(manager.snapshotId).toBe("snapshot-2"); + expect(manager.status).toBe("EXECUTING"); + }); + + it("should handle suspendable state changes", async () => { + await manager.setSuspendable(true); + expect(manager.suspendable).toBe(true); + expect(mockSuspendableHandler).not.toHaveBeenCalled(); + + // When status changes to EXECUTING_WITH_WAITPOINTS, suspendable handler should be called + await manager.handleSnapshotChange( + createRunExecutionData({ + snapshotId: "snapshot-2", + executionStatus: "EXECUTING_WITH_WAITPOINTS", + }) + ); + + expect(mockSuspendableHandler).toHaveBeenCalledWith({ + id: "snapshot-2", + status: "EXECUTING_WITH_WAITPOINTS", + }); + + // Reset mocks + vi.clearAllMocks(); + + // Test this the other way around + await manager.setSuspendable(false); + expect(manager.suspendable).toBe(false); + expect(mockSuspendableHandler).not.toHaveBeenCalled(); + + // We should still be EXECUTING_WITH_WAITPOINTS + expect(manager.status).toBe("EXECUTING_WITH_WAITPOINTS"); + + // When we're suspendable again, the handler should be called + await manager.setSuspendable(true); + expect(manager.suspendable).toBe(true); + expect(mockSuspendableHandler).toHaveBeenCalledWith({ + id: "snapshot-2", + status: "EXECUTING_WITH_WAITPOINTS", + }); + + // Reset mocks + vi.clearAllMocks(); + + // Check simple toggle + await manager.setSuspendable(false); + expect(manager.suspendable).toBe(false); + await manager.setSuspendable(true); + expect(manager.suspendable).toBe(true); + expect(mockSuspendableHandler).toHaveBeenCalledWith({ + id: "snapshot-2", + status: "EXECUTING_WITH_WAITPOINTS", + }); + + // Reset mocks + vi.clearAllMocks(); + + // This should also work with QUEUED_EXECUTING + await manager.setSuspendable(false); + expect(manager.suspendable).toBe(false); + + // Update the snapshot to QUEUED_EXECUTING + await manager.handleSnapshotChange( + createRunExecutionData({ + snapshotId: "snapshot-3", + executionStatus: "QUEUED_EXECUTING", + }) + ); + expect(mockSuspendableHandler).not.toHaveBeenCalled(); + + // Set suspendable to true and check that the handler is called + await manager.setSuspendable(true); + expect(manager.suspendable).toBe(true); + expect(mockSuspendableHandler).toHaveBeenCalledWith({ + id: "snapshot-3", + status: "QUEUED_EXECUTING", + }); + }); + + it("should process queue in correct order with suspendable changes at the back", async () => { + const executionOrder: string[] = []; + + // Create a manager with handlers that track execution order + const manager = new SnapshotManager({ + runFriendlyId: "test-run-1", + initialSnapshotId: "snapshot-1", + initialStatus: "PENDING_EXECUTING", + logger: mockLogger, + onSnapshotChange: async (data) => { + executionOrder.push(`snapshot:${data.snapshot.friendlyId}`); + await setTimeout(10); // Small delay + }, + onSuspendable: async (state) => { + executionOrder.push(`suspendable:${state.id}`); + await setTimeout(10); // Small delay + }, + }); + + const promises = [ + manager.setSuspendable(false), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.setSuspendable(true), + manager.handleSnapshotChange( + createRunExecutionData({ + snapshotId: "snapshot-3", + executionStatus: "EXECUTING_WITH_WAITPOINTS", + }) + ), + ]; + + await Promise.all(promises); + + // Verify execution order: + // 1. Snapshots should be processed in order (2 then 3) + // 2. Suspendable changes should be at the end + expect(executionOrder).toEqual([ + "snapshot:snapshot-2", + "snapshot:snapshot-3", + "suspendable:snapshot-3", + ]); + }); + + it("should skip older snapshots", async () => { + const executionOrder: string[] = []; + + const manager = new SnapshotManager({ + runFriendlyId: "test-run-1", + initialSnapshotId: "snapshot-1", + initialStatus: "PENDING_EXECUTING", + logger: mockLogger, + onSnapshotChange: async (data) => { + executionOrder.push(`snapshot:${data.snapshot.friendlyId}`); + }, + onSuspendable: async () => {}, + }); + + // Queue snapshots in reverse order + const promises = [ + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-3" })), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-1" })), + ]; + + await Promise.all(promises); + + // Should be processed in ID order + expect(executionOrder).toEqual(["snapshot:snapshot-3"]); + }); + + it("should skip duplicate snapshots", async () => { + const executionOrder: string[] = []; + + const manager = new SnapshotManager({ + runFriendlyId: "test-run-1", + initialSnapshotId: "snapshot-1", + initialStatus: "PENDING_EXECUTING", + logger: mockLogger, + onSnapshotChange: async (data) => { + executionOrder.push(`snapshot:${data.snapshot.friendlyId}`); + }, + onSuspendable: async () => {}, + }); + + // Queue snapshots in reverse order + const promises = [ + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + ]; + + await Promise.all(promises); + + // Should be processed in ID order + expect(executionOrder).toEqual(["snapshot:snapshot-2"]); + }); + + it("should prevent concurrent handler execution", async () => { + const executionTimes: { start: number; end: number; type: string }[] = []; + let currentlyExecuting = false; + + const manager = new SnapshotManager({ + runFriendlyId: "test-run-1", + initialSnapshotId: "snapshot-1", + initialStatus: "PENDING_EXECUTING", + logger: mockLogger, + onSnapshotChange: async (data) => { + if (currentlyExecuting) { + throw new Error("Handler executed while another handler was running"); + } + currentlyExecuting = true; + const start = Date.now(); + await setTimeout(20); // Deliberate delay to increase chance of catching concurrent execution + const end = Date.now(); + executionTimes.push({ start, end, type: `snapshot:${data.snapshot.friendlyId}` }); + currentlyExecuting = false; + }, + onSuspendable: async (state) => { + if (currentlyExecuting) { + throw new Error("Handler executed while another handler was running"); + } + currentlyExecuting = true; + const start = Date.now(); + await setTimeout(20); // Deliberate delay + const end = Date.now(); + executionTimes.push({ start, end, type: `suspendable:${state.id}` }); + currentlyExecuting = false; + }, + }); + + // Create a mix of rapid changes + const promises = [ + manager.setSuspendable(true), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.setSuspendable(false), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-3" })), + manager.setSuspendable(true), + manager.setSuspendable(true), + manager.setSuspendable(false), + manager.setSuspendable(false), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-4" })), + manager.setSuspendable(false), + manager.setSuspendable(true), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-1" })), + manager.setSuspendable(false), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-3" })), + manager.setSuspendable(true), + manager.setSuspendable(true), + manager.setSuspendable(false), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.setSuspendable(false), + ]; + + await Promise.all(promises); + + // Verify no overlapping execution times + for (let i = 1; i < executionTimes.length; i++) { + const previous = executionTimes[i - 1]!; + const current = executionTimes[i]!; + expect(current.start).toBeGreaterThanOrEqual(previous.end); + } + }); + + it("should handle cleanup and error scenarios", async () => { + const executionOrder: string[] = []; + let shouldThrowSnapshotError = false; + let shouldThrowSuspendableError = false; + + const manager = new SnapshotManager({ + runFriendlyId: "test-run-1", + initialSnapshotId: "snapshot-1", + initialStatus: "PENDING_EXECUTING", + logger: mockLogger, + onSnapshotChange: async (data) => { + if (shouldThrowSnapshotError) { + throw new Error("Snapshot handler error"); + } + executionOrder.push(`snapshot:${data.snapshot.friendlyId}`); + await setTimeout(10); + }, + onSuspendable: async (state) => { + if (shouldThrowSuspendableError) { + throw new Error("Suspendable handler error"); + } + executionOrder.push(`suspendable:${state.id}`); + await setTimeout(10); + }, + }); + + // Queue up some changes + const initialPromises = [ + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.setSuspendable(true), + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-3" })), + ]; + + expect(manager.queueLength).not.toBe(0); + + // Call cleanup before they complete + manager.cleanup(); + + expect(manager.queueLength).toBe(0); + + // These should complete without executing handlers + const results = await Promise.allSettled(initialPromises); + + // Only the first snapshot should have been processed + expect(executionOrder).toEqual(["snapshot:snapshot-2"]); + + // The last two promises should have been rejected + expect(results).toMatchObject([ + { status: "fulfilled" }, + { status: "rejected" }, + { status: "rejected" }, + ]); + + // Now test error handling + shouldThrowSnapshotError = true; + await expect( + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-4" })) + ).rejects.toThrow("Snapshot handler error"); + + // Queue should continue processing after error + shouldThrowSnapshotError = false; + await manager.handleSnapshotChange( + createRunExecutionData({ + snapshotId: "snapshot-5", + executionStatus: "EXECUTING_WITH_WAITPOINTS", + }) + ); + expect(executionOrder).toEqual(["snapshot:snapshot-2", "snapshot:snapshot-5"]); + + // Test suspendable error + shouldThrowSuspendableError = true; + await expect(manager.setSuspendable(true)).rejects.toThrow("Suspendable handler error"); + + // Queue should continue processing after suspendable error + shouldThrowSuspendableError = false; + + // Toggle suspendable state to trigger handler + await manager.setSuspendable(false); + await manager.setSuspendable(true); + + expect(executionOrder).toEqual([ + "snapshot:snapshot-2", + "snapshot:snapshot-5", + "suspendable:snapshot-5", + ]); + }); + + it("should handle edge cases and high concurrency", async () => { + const executionOrder: string[] = []; + const executionTimes: { start: number; end: number; type: string }[] = []; + let currentlyExecuting = false; + let handlerExecutionCount = 0; + + const manager = new SnapshotManager({ + runFriendlyId: "test-run-1", + initialSnapshotId: "snapshot-1", + initialStatus: "PENDING_EXECUTING", + logger: mockLogger, + onSnapshotChange: async (data) => { + if (currentlyExecuting) { + throw new Error("Handler executed while another handler was running"); + } + currentlyExecuting = true; + handlerExecutionCount++; + + const start = Date.now(); + executionOrder.push(`snapshot:${data.snapshot.friendlyId}`); + await setTimeout(Math.random() * 20); // Random delay to increase race condition chances + const end = Date.now(); + + executionTimes.push({ start, end, type: `snapshot:${data.snapshot.friendlyId}` }); + currentlyExecuting = false; + }, + onSuspendable: async (state) => { + if (currentlyExecuting) { + throw new Error("Handler executed while another handler was running"); + } + currentlyExecuting = true; + handlerExecutionCount++; + + const start = Date.now(); + executionOrder.push(`suspendable:${state.id}`); + await setTimeout(Math.random() * 20); // Random delay + const end = Date.now(); + + executionTimes.push({ start, end, type: `suspendable:${state.id}` }); + currentlyExecuting = false; + }, + }); + + // Test empty snapshot IDs + await manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "" })); + expect(executionOrder).toEqual([]); + + // Create a very long queue of mixed changes + const promises: Promise[] = []; + + // Add 50 mixed changes + for (let i = 1; i <= 50; i++) { + if (i % 2 === 0) { + promises.push( + manager.handleSnapshotChange(createRunExecutionData({ snapshotId: `snapshot-${i}` })) + ); + } else { + promises.push(manager.setSuspendable(i % 4 === 1)); + } + } + + // Add rapid toggling of suspendable state + for (let i = 0; i < 20; i++) { + promises.push(manager.setSuspendable(i % 2 === 0)); + } + + // Add overlapping snapshot changes + const snapshotIds = ["A", "B", "C", "D", "E"]; + for (const id of snapshotIds) { + for (let i = 0; i < 5; i++) { + promises.push( + manager.handleSnapshotChange( + createRunExecutionData({ snapshotId: `snapshot-${id}-${i}` }) + ) + ); + } + } + + console.log(manager.queueLength); + + await Promise.all(promises); + + // Verify handler execution exclusivity + for (let i = 1; i < executionTimes.length; i++) { + const previous = executionTimes[i - 1]!; + const current = executionTimes[i]!; + expect(current.start).toBeGreaterThanOrEqual(previous.end); + } + + // Verify all handlers executed in sequence + expect(currentlyExecuting).toBe(false); + + // Verify suspendable state is correctly maintained + const finalSuspendableState = manager.suspendable; + const lastSuspendableChange = executionOrder + .filter((entry) => entry.startsWith("suspendable:")) + .pop(); + + // The last recorded suspendable change should match the final state + if (finalSuspendableState) { + expect(lastSuspendableChange).toBeDefined(); + } + + // Verify snapshot ordering + const snapshotExecutions = executionOrder + .filter((entry) => entry.startsWith("snapshot:")) + .map((entry) => entry.split(":")[1]); + + // Each snapshot should be greater than the previous one + for (let i = 1; i < snapshotExecutions.length; i++) { + expect(snapshotExecutions[i]! > snapshotExecutions[i - 1]!).toBe(true); + } + }); + + it("should handle queue processing and remaining edge cases", async () => { + const executionOrder: string[] = []; + let processingCount = 0; + + const manager = new SnapshotManager({ + runFriendlyId: "test-run-1", + initialSnapshotId: "snapshot-1", + initialStatus: "PENDING_EXECUTING", + logger: mockLogger, + onSnapshotChange: async (data) => { + processingCount++; + executionOrder.push(`snapshot:${data.snapshot.friendlyId}`); + await setTimeout(10); + processingCount--; + }, + onSuspendable: async (state) => { + processingCount++; + executionOrder.push(`suspendable:${state.id}`); + await setTimeout(10); + processingCount--; + }, + }); + + // Test parallel queue processing prevention + const parallelPromises = Array.from({ length: 5 }, (_, i) => + manager.handleSnapshotChange( + createRunExecutionData({ + snapshotId: `parallel-${i}`, + executionStatus: "EXECUTING", + }) + ) + ); + + // Add some suspendable changes in the middle + parallelPromises.push(manager.setSuspendable(true)); + parallelPromises.push(manager.setSuspendable(false)); + + // Add more snapshot changes + parallelPromises.push( + ...Array.from({ length: 5 }, (_, i) => + manager.handleSnapshotChange( + createRunExecutionData({ + snapshotId: `parallel-${i + 5}`, + executionStatus: "EXECUTING", + }) + ) + ) + ); + + await Promise.all(parallelPromises); + + // Verify processingCount never exceeded 1 + expect(processingCount).toBe(0); + + // Test edge case: snapshot ID comparison with special characters + const specialCharPromises = [ + manager.handleSnapshotChange( + createRunExecutionData({ + snapshotId: "snapshot-1!", + executionStatus: "EXECUTING", + }) + ), + manager.handleSnapshotChange( + createRunExecutionData({ + snapshotId: "snapshot-1@", + executionStatus: "EXECUTING", + }) + ), + manager.handleSnapshotChange( + createRunExecutionData({ + snapshotId: "snapshot-1#", + executionStatus: "EXECUTING", + }) + ), + ]; + + await Promise.all(specialCharPromises); + + // Test edge case: very long snapshot IDs + const longIdPromises = [ + manager.handleSnapshotChange( + createRunExecutionData({ + snapshotId: "a".repeat(1000), + executionStatus: "EXECUTING", + }) + ), + manager.handleSnapshotChange( + createRunExecutionData({ + snapshotId: "b".repeat(1000), + executionStatus: "EXECUTING", + }) + ), + ]; + + await Promise.all(longIdPromises); + + // Test edge case: rapid queue changes during processing + let isProcessing = false; + const rapidChangeManager = new SnapshotManager({ + runFriendlyId: "test-run-2", + initialSnapshotId: "snapshot-1", + initialStatus: "PENDING_EXECUTING", + logger: mockLogger, + onSnapshotChange: async (data) => { + if (isProcessing) { + throw new Error("Parallel processing detected"); + } + isProcessing = true; + await setTimeout(50); // Longer delay to test queue changes during processing + executionOrder.push(`rapid:${data.snapshot.friendlyId}`); + isProcessing = false; + }, + onSuspendable: async () => {}, + }); + + // Start processing a snapshot + const initialPromise = rapidChangeManager.handleSnapshotChange( + createRunExecutionData({ + runId: "test-run-2", + snapshotId: "snapshot-2", + executionStatus: "EXECUTING", + }) + ); + + // Queue more changes while the first one is processing + await setTimeout(10); + const queuePromises = [ + rapidChangeManager.handleSnapshotChange( + createRunExecutionData({ + runId: "test-run-2", + snapshotId: "snapshot-3", + executionStatus: "EXECUTING", + }) + ), + rapidChangeManager.handleSnapshotChange( + createRunExecutionData({ + runId: "test-run-2", + snapshotId: "snapshot-4", + executionStatus: "EXECUTING", + }) + ), + ]; + + await Promise.all([initialPromise, ...queuePromises]); + + // Verify the rapid changes were processed in order + const rapidChanges = executionOrder.filter((entry) => entry.startsWith("rapid:")); + expect(rapidChanges).toEqual(["rapid:snapshot-2", "rapid:snapshot-3", "rapid:snapshot-4"]); + }); +}); + +// Helper to generate RunExecutionData with sensible defaults +function createRunExecutionData( + overrides: { + runId?: string; + runFriendlyId?: string; + snapshotId?: string; + snapshotFriendlyId?: string; + executionStatus?: TaskRunExecutionStatus; + description?: string; + } = {} +): RunExecutionData { + const runId = overrides.runId ?? "test-run-1"; + const runFriendlyId = overrides.runFriendlyId ?? runId; + const snapshotId = overrides.snapshotId ?? "snapshot-1"; + const snapshotFriendlyId = overrides.snapshotFriendlyId ?? snapshotId; + + return { + version: "1" as const, + run: { + id: runId, + friendlyId: runFriendlyId, + status: "EXECUTING", + attemptNumber: 1, + }, + snapshot: { + id: snapshotId, + friendlyId: snapshotFriendlyId, + executionStatus: overrides.executionStatus ?? "EXECUTING", + description: overrides.description ?? "Test snapshot", + }, + completedWaitpoints: [], + }; +} diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.ts index f23590b821..c1387f8d4a 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.ts @@ -21,8 +21,8 @@ type SnapshotManagerOptions = { }; type QueuedChange = - | { type: "snapshot"; data: RunExecutionData } - | { type: "suspendable"; value: boolean }; + | { id: string; type: "snapshot"; data: RunExecutionData } + | { id: string; type: "suspendable"; value: boolean }; type QueuedChangeItem = { change: QueuedChange; @@ -72,7 +72,11 @@ export class SnapshotManager { this.sendDebugLog(`setting suspendable to ${suspendable}`); - return this.enqueueSnapshotChange({ type: "suspendable", value: suspendable }); + return this.enqueueSnapshotChange({ + id: crypto.randomUUID(), + type: "suspendable", + value: suspendable, + }); } /** @@ -99,7 +103,15 @@ export class SnapshotManager { return; } - return this.enqueueSnapshotChange({ type: "snapshot", data: runData }); + return this.enqueueSnapshotChange({ + id: crypto.randomUUID(), + type: "snapshot", + data: runData, + }); + } + + public get queueLength(): number { + return this.changeQueue.length; } private statusCheck(runData: RunExecutionData): boolean { @@ -144,18 +156,20 @@ export class SnapshotManager { private async enqueueSnapshotChange(change: QueuedChange): Promise { return new Promise((resolve, reject) => { - // For suspendable changes, resolve and remove any pending suspendable changes - // since only the last one matters + // For suspendable changes, resolve and remove any pending suspendable changes since only the last one matters if (change.type === "suspendable") { const pendingSuspendable = this.changeQueue.filter( (item) => item.change.type === "suspendable" ); // Resolve any pending suspendable changes - they're effectively done since we're superseding them - pendingSuspendable.forEach((item) => item.resolve()); + for (const item of pendingSuspendable) { + item.resolve(); + } - // Remove them from the queue - this.changeQueue = this.changeQueue.filter((item) => item.change.type !== "suspendable"); + // Remove the exact items we just resolved + const resolvedIds = new Set(pendingSuspendable.map((item) => item.change.id)); + this.changeQueue = this.changeQueue.filter((item) => !resolvedIds.has(item.change.id)); } this.changeQueue.push({ change, resolve, reject }); @@ -192,16 +206,16 @@ export class SnapshotManager { this.isProcessingQueue = true; try { - while (this.changeQueue.length > 0) { - const item = this.changeQueue[0]; + while (this.queueLength > 0) { + // Remove first item from queue + const item = this.changeQueue.shift(); if (!item) { break; } const [error] = await tryCatch(this.applyChange(item.change)); - // Remove from queue and resolve/reject promise - this.changeQueue.shift(); + // Resolve/reject promise if (error) { item.reject(error); } else { @@ -209,7 +223,14 @@ export class SnapshotManager { } } } finally { + const hasMoreItems = this.queueLength > 0; this.isProcessingQueue = false; + + if (hasMoreItems) { + this.processQueue().catch((error) => { + this.sendDebugLog("error processing queue (finally)", { error: error.message }); + }); + } } } @@ -221,11 +242,10 @@ export class SnapshotManager { return; } const { snapshot } = change.data; + const oldState = { ...this.state }; this.updateSnapshot(snapshot.friendlyId, snapshot.executionStatus); - const oldState = { ...this.state }; - this.sendDebugLog(`status changed to ${snapshot.executionStatus}`, { oldId: oldState.id, newId: snapshot.friendlyId, @@ -266,6 +286,9 @@ export class SnapshotManager { public cleanup() { // Clear any pending changes + for (const item of this.changeQueue) { + item.reject(new Error("SnapshotManager cleanup")); + } this.changeQueue = []; } @@ -278,7 +301,7 @@ export class SnapshotManager { snapshotId: this.state.id, status: this.state.status, suspendable: this.isSuspendable, - queueLength: this.changeQueue.length, + queueLength: this.queueLength, isProcessingQueue: this.isProcessingQueue, }, }); From ddb40ae266e20009bb15ff4ef817799c67817182 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 01:02:03 +0100 Subject: [PATCH 27/62] fix cli builds --- packages/cli-v3/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli-v3/package.json b/packages/cli-v3/package.json index 059ad05abe..29c8262ff0 100644 --- a/packages/cli-v3/package.json +++ b/packages/cli-v3/package.json @@ -39,6 +39,9 @@ "esm" ], "project": "./tsconfig.src.json", + "exclude": [ + "**/*.test.ts" + ], "exports": { "./package.json": "./package.json", ".": "./src/index.ts" From 54db582e5ca5c796e089287dec14ccdf178c7e6a Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 09:17:56 +0100 Subject: [PATCH 28/62] improve QUEUED_EXECUTING test --- .../src/entryPoints/managed/snapshot.test.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts index 18e93b0497..256db83cfe 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts @@ -1,10 +1,11 @@ import { SnapshotManager } from "./snapshot.js"; import { ConsoleRunLogger } from "./logger.js"; -import { RunExecutionData, TaskRunExecutionStatus, TaskRunStatus } from "@trigger.dev/core/v3"; +import { RunExecutionData, TaskRunExecutionStatus } from "@trigger.dev/core/v3"; import { setTimeout } from "timers/promises"; +import { isCI } from "std-env"; describe("SnapshotManager", () => { - const mockLogger = new ConsoleRunLogger(); + const mockLogger = new ConsoleRunLogger({ print: !isCI }); const mockSnapshotHandler = vi.fn(); const mockSuspendableHandler = vi.fn(); @@ -94,22 +95,13 @@ describe("SnapshotManager", () => { // Reset mocks vi.clearAllMocks(); - // This should also work with QUEUED_EXECUTING - await manager.setSuspendable(false); - expect(manager.suspendable).toBe(false); - - // Update the snapshot to QUEUED_EXECUTING + // Transitioning to QUEUED_EXECUTING should call the handler again await manager.handleSnapshotChange( createRunExecutionData({ snapshotId: "snapshot-3", executionStatus: "QUEUED_EXECUTING", }) ); - expect(mockSuspendableHandler).not.toHaveBeenCalled(); - - // Set suspendable to true and check that the handler is called - await manager.setSuspendable(true); - expect(manager.suspendable).toBe(true); expect(mockSuspendableHandler).toHaveBeenCalledWith({ id: "snapshot-3", status: "QUEUED_EXECUTING", @@ -444,8 +436,6 @@ describe("SnapshotManager", () => { } } - console.log(manager.queueLength); - await Promise.all(promises); // Verify handler execution exclusivity From b17a9476227d16b4f27d57ff8b8e0adc9343dc07 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 09:22:32 +0100 Subject: [PATCH 29/62] changeset --- .changeset/plenty-dolphins-act.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/plenty-dolphins-act.md diff --git a/.changeset/plenty-dolphins-act.md b/.changeset/plenty-dolphins-act.md new file mode 100644 index 0000000000..59d2c7fc44 --- /dev/null +++ b/.changeset/plenty-dolphins-act.md @@ -0,0 +1,8 @@ +--- +"trigger.dev": patch +"@trigger.dev/core": patch +--- + +- Correctly resolve waitpoints that come in early +- Ensure correct state before requesting suspension +- Fix race conditions in snapshot processing From 68ea4c488198f951a230efda4478a8aa644c36f6 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 11:57:41 +0100 Subject: [PATCH 30/62] make testcontainers wait until container has stopped --- internal-packages/testcontainers/src/index.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/internal-packages/testcontainers/src/index.ts b/internal-packages/testcontainers/src/index.ts index 7253470998..9debf8fce8 100644 --- a/internal-packages/testcontainers/src/index.ts +++ b/internal-packages/testcontainers/src/index.ts @@ -53,7 +53,9 @@ const postgresContainer = async ( try { await use(container); } finally { - await container.stop(); + // WARNING: Testcontainers by default will not wait until the container has stopped. It will simply issue the stop command and return immediately. + // If you need to wait for the container to be stopped, you can provide a timeout. The unit of timeout option here is second + await container.stop({ timeout: 10 }); } }; @@ -92,7 +94,9 @@ const redisContainer = async ( try { await use(container); } finally { - await container.stop(); + // WARNING: Testcontainers by default will not wait until the container has stopped. It will simply issue the stop command and return immediately. + // If you need to wait for the container to be stopped, you can provide a timeout. The unit of timeout option here is second + await container.stop({ timeout: 10 }); } }; @@ -142,7 +146,10 @@ const electricOrigin = async ( try { await use(origin); } finally { - await container.stop(); + // WARNING: Testcontainers by default will not wait until the container has stopped. It will simply issue the stop command and return immediately. + // If you need to wait for the container to be stopped, you can provide a timeout. The unit of timeout option here is second + + await container.stop({ timeout: 10 }); } }; From c36e2746b64d2c2261d772917cb45ab8616d5268 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 13:28:13 +0100 Subject: [PATCH 31/62] require unit tests for publishing again --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ecf30bcc8b..0f011c6cad 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -56,14 +56,14 @@ jobs: secrets: inherit publish-webapp: - needs: [typecheck] + needs: [typecheck, units] uses: ./.github/workflows/publish-webapp.yml secrets: inherit with: image_tag: ${{ inputs.image_tag }} publish-worker: - needs: [typecheck] + needs: [typecheck, units] uses: ./.github/workflows/publish-worker.yml secrets: inherit with: From 87b0ce1e5b63742af8019ad285fa6a5f868e5594 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 15:31:52 +0100 Subject: [PATCH 32/62] avoid mutation during iteration when resolving pending waitpoints --- packages/core/src/v3/runtime/sharedRuntimeManager.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/src/v3/runtime/sharedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts index acf734c05d..7a881d4076 100644 --- a/packages/core/src/v3/runtime/sharedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -259,7 +259,14 @@ export class SharedRuntimeManager implements RuntimeManager { } private resolvePendingWaitpoints(): void { - for (const [resolverId, waitpoint] of this.waitpointsByResolverId.entries()) { + // Clone keys first to avoid mutation-during-iteration hazards + for (const resolverId of Array.from(this.waitpointsByResolverId.keys())) { + const waitpoint = this.waitpointsByResolverId.get(resolverId); + + if (!waitpoint) { + continue; + } + this.resolveWaitpoint(waitpoint, resolverId); } } From 2622b0dd5a2aef092e97129a1391b8300950fe2e Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 15:50:02 +0100 Subject: [PATCH 33/62] improve debug logs and make them less noisy --- internal-packages/testcontainers/src/index.ts | 1 - .../src/entryPoints/managed/execution.ts | 23 +++++++++++-------- .../src/entryPoints/managed/snapshot.ts | 10 ++++---- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/internal-packages/testcontainers/src/index.ts b/internal-packages/testcontainers/src/index.ts index 9debf8fce8..ae5dcc76d6 100644 --- a/internal-packages/testcontainers/src/index.ts +++ b/internal-packages/testcontainers/src/index.ts @@ -148,7 +148,6 @@ const electricOrigin = async ( } finally { // WARNING: Testcontainers by default will not wait until the container has stopped. It will simply issue the stop command and return immediately. // If you need to wait for the container to be stopped, you can provide a timeout. The unit of timeout option here is second - await container.stop({ timeout: 10 }); } }; diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 1095858ce5..4f3a5fb429 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -208,13 +208,16 @@ export class RunExecution { return; } - this.sendDebugLog(`snapshot has changed to: ${snapshot.executionStatus}`, snapshotMetadata); + // DO NOT REMOVE (very noisy, but helpful for debugging) + // this.sendDebugLog(`processing snapshot change: ${snapshot.executionStatus}`, snapshotMetadata); // Reset the snapshot poll interval so we don't do unnecessary work this.snapshotPoller?.resetCurrentInterval(); switch (snapshot.executionStatus) { case "PENDING_CANCEL": { + this.sendDebugLog("run was cancelled", snapshotMetadata); + const [error] = await tryCatch(this.cancel()); if (error) { @@ -279,13 +282,12 @@ export class RunExecution { return; } case "EXECUTING": { - this.sendDebugLog("run is now executing", snapshotMetadata); - if (completedWaitpoints.length === 0) { + this.sendDebugLog("run is executing without completed waitpoints", snapshotMetadata); return; } - this.sendDebugLog("processing completed waitpoints", snapshotMetadata); + this.sendDebugLog("run is executing with completed waitpoints", snapshotMetadata); if (!this.taskRunProcess) { this.sendDebugLog("no task run process, ignoring completed waitpoints", snapshotMetadata); @@ -301,7 +303,10 @@ export class RunExecution { return; } case "RUN_CREATED": { - this.sendDebugLog("invalid status change", snapshotMetadata); + this.sendDebugLog( + "aborting execution: invalid status change: RUN_CREATED", + snapshotMetadata + ); this.abortExecution(); return; @@ -627,7 +632,7 @@ export class RunExecution { const snapshotStatus = this.convertAttemptStatusToSnapshotStatus(result.attemptStatus); // Update our snapshot ID to match the completion result to ensure any subsequent API calls use the correct snapshot - this.updateSnapshot(result.snapshot.friendlyId, snapshotStatus); + this.updateSnapshotAfterCompletion(result.snapshot.friendlyId, snapshotStatus); const { attemptStatus } = result; @@ -664,7 +669,7 @@ export class RunExecution { assertExhaustive(attemptStatus); } - private updateSnapshot(snapshotId: string, status: TaskRunExecutionStatus) { + private updateSnapshotAfterCompletion(snapshotId: string, status: TaskRunExecutionStatus) { this.snapshotManager?.updateSnapshot(snapshotId, status); this.snapshotPoller?.updateSnapshotId(snapshotId); } @@ -1000,7 +1005,7 @@ export class RunExecution { ); if (!suspendResult.success) { - this.sendDebugLog("failed to suspend run, staying alive 🎶", { + this.sendDebugLog("suspension request failed, staying alive 🎶", { suspendableSnapshot, error: suspendResult.error, }); @@ -1010,7 +1015,7 @@ export class RunExecution { } if (!suspendResult.data.ok) { - this.sendDebugLog("checkpoint: failed to suspend run", { + this.sendDebugLog("suspension request returned error, staying alive 🎶", { suspendableSnapshot, error: suspendResult.data.error, }); diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.ts index c1387f8d4a..ce8b5ad3e4 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.ts @@ -144,9 +144,10 @@ export class SnapshotManager { // Skip if this is the current snapshot if (snapshot.friendlyId === this.state.id) { - this.sendDebugLog("skipping update for duplicate snapshot", { - statusCheckData, - }); + // DO NOT REMOVE (very noisy, but helpful for debugging) + // this.sendDebugLog("skipping update for duplicate snapshot", { + // statusCheckData, + // }); return false; } @@ -279,7 +280,8 @@ export class SnapshotManager { (this.state.status === "EXECUTING_WITH_WAITPOINTS" || this.state.status === "QUEUED_EXECUTING") ) { - this.sendDebugLog("run is now suspendable, executing handler"); + // DO NOT REMOVE (very noisy, but helpful for debugging) + // this.sendDebugLog("run is now suspendable, executing handler"); await this.onSuspendable(this.state); } } From ffa2a733b8cd9f6e4241237d5960819c1cc6644c Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 15:50:49 +0100 Subject: [PATCH 34/62] always update poller snapshot id for accurate logs --- packages/cli-v3/src/entryPoints/managed/execution.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 4f3a5fb429..16b863685a 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -212,6 +212,7 @@ export class RunExecution { // this.sendDebugLog(`processing snapshot change: ${snapshot.executionStatus}`, snapshotMetadata); // Reset the snapshot poll interval so we don't do unnecessary work + this.snapshotPoller?.updateSnapshotId(snapshot.friendlyId); this.snapshotPoller?.resetCurrentInterval(); switch (snapshot.executionStatus) { From 7a37a26474680f3c2acf9a1eadc066e6f3d30d8b Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 15:51:07 +0100 Subject: [PATCH 35/62] detach task run process handlers --- packages/cli-v3/src/entryPoints/managed/execution.ts | 4 +++- packages/cli-v3/src/executions/taskRunProcess.ts | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 16b863685a..1f104763b9 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -950,9 +950,11 @@ export class RunExecution { } this.isShuttingDown = true; + this.snapshotPoller?.stop(); - this.taskRunProcess?.onTaskRunHeartbeat.detach(); this.snapshotManager?.cleanup(); + + this.taskRunProcess?.unsafeDetachEvtHandlers(); } private async handleSuspendable(suspendableSnapshot: SnapshotState) { diff --git a/packages/cli-v3/src/executions/taskRunProcess.ts b/packages/cli-v3/src/executions/taskRunProcess.ts index 611c9a3556..6ac384cc03 100644 --- a/packages/cli-v3/src/executions/taskRunProcess.ts +++ b/packages/cli-v3/src/executions/taskRunProcess.ts @@ -97,6 +97,13 @@ export class TaskRunProcess { return this._isPreparedForNextAttempt; } + unsafeDetachEvtHandlers() { + this.onExit.detach(); + this.onIsBeingKilled.detach(); + this.onSendDebugLog.detach(); + this.onSetSuspendable.detach(); + } + async cancel() { this._isPreparedForNextRun = false; this._isBeingCancelled = true; From 8fd09e39b2e2ef4495ec715081ab28ad116b6fb9 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 17:30:28 +0100 Subject: [PATCH 36/62] check for env overrides in a few more places and add verbose logs --- .../src/entryPoints/managed/controller.ts | 26 ++++++++++++++++++- .../src/entryPoints/managed/execution.ts | 22 ++++++++++++---- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/controller.ts b/packages/cli-v3/src/entryPoints/managed/controller.ts index 3c7ab6c5e8..3c6bef8138 100644 --- a/packages/cli-v3/src/entryPoints/managed/controller.ts +++ b/packages/cli-v3/src/entryPoints/managed/controller.ts @@ -524,7 +524,7 @@ export class ManagedRunController { }); }); - socket.on("disconnect", (reason, description) => { + socket.on("disconnect", async (reason, description) => { const parseDescription = (): | { description: string; @@ -547,6 +547,30 @@ export class ManagedRunController { }; }; + if (this.currentExecution) { + const currentEnv = { + workerInstanceName: this.env.TRIGGER_WORKER_INSTANCE_NAME, + runnerId: this.env.TRIGGER_RUNNER_ID, + supervisorApiUrl: this.env.TRIGGER_SUPERVISOR_API_URL, + }; + + await this.currentExecution.processEnvOverrides("socket disconnected"); + + const newEnv = { + workerInstanceName: this.env.TRIGGER_WORKER_INSTANCE_NAME, + runnerId: this.env.TRIGGER_RUNNER_ID, + supervisorApiUrl: this.env.TRIGGER_SUPERVISOR_API_URL, + }; + + this.sendDebugLog({ + runId: this.runFriendlyId, + message: "Socket disconnected from supervisor - processed env overrides", + properties: { reason, ...parseDescription(), currentEnv, newEnv }, + }); + + return; + } + this.sendDebugLog({ runId: this.runFriendlyId, message: "Socket disconnected from supervisor", diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 1f104763b9..fae6206a9b 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -215,6 +215,8 @@ export class RunExecution { this.snapshotPoller?.updateSnapshotId(snapshot.friendlyId); this.snapshotPoller?.resetCurrentInterval(); + await this.processEnvOverrides("snapshot change"); + switch (snapshot.executionStatus) { case "PENDING_CANCEL": { this.sendDebugLog("run was cancelled", snapshotMetadata); @@ -780,7 +782,7 @@ export class RunExecution { await sleep(100); // Process any env overrides - await this.processEnvOverrides(); + await this.processEnvOverrides("restore"); const continuationResult = await this.httpClient.continueRunExecution( this.runFriendlyId, @@ -798,9 +800,9 @@ export class RunExecution { /** * Processes env overrides from the metadata service. Generally called when we're resuming from a suspended state. */ - private async processEnvOverrides() { + async processEnvOverrides(reason?: string) { if (!this.env.TRIGGER_METADATA_URL) { - this.sendDebugLog("no metadata url, skipping env overrides"); + this.sendDebugLog("no metadata url, skipping env overrides", { reason }); return; } @@ -808,11 +810,21 @@ export class RunExecution { const overrides = await metadataClient.getEnvOverrides(); if (!overrides) { - this.sendDebugLog("no env overrides, skipping"); + this.sendDebugLog("no env overrides, skipping", { reason }); return; } - this.sendDebugLog("processing env overrides", overrides); + this.sendDebugLog(`processing env overrides: ${reason}`, { + overrides, + currentEnv: this.env.raw, + }); + + if (this.env.TRIGGER_RUNNER_ID !== overrides.TRIGGER_RUNNER_ID) { + this.sendDebugLog("runner ID changed -> run was restored from a checkpoint", { + currentRunnerId: this.env.TRIGGER_RUNNER_ID, + newRunnerId: overrides.TRIGGER_RUNNER_ID, + }); + } // Override the env with the new values this.env.override(overrides); From da24a791548af96c9ee0a7e5d1b1edd0a6b57b71 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 20:14:44 +0100 Subject: [PATCH 37/62] log when poller is still executing when we stop it --- packages/cli-v3/src/entryPoints/managed/poller.ts | 7 ++++++- packages/core/src/v3/utils/interval.ts | 10 ++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/poller.ts b/packages/cli-v3/src/entryPoints/managed/poller.ts index 148bbbbb15..31b04be4ce 100644 --- a/packages/cli-v3/src/entryPoints/managed/poller.ts +++ b/packages/cli-v3/src/entryPoints/managed/poller.ts @@ -110,6 +110,11 @@ export class RunExecutionSnapshotPoller { } this.enabled = false; - this.poller.stop(); + + const { isExecuting } = this.poller.stop(); + + if (isExecuting) { + this.sendDebugLog("stopped poller but it's still executing"); + } } } diff --git a/packages/core/src/v3/utils/interval.ts b/packages/core/src/v3/utils/interval.ts index 9470ae2bb2..7ea7434718 100644 --- a/packages/core/src/v3/utils/interval.ts +++ b/packages/core/src/v3/utils/interval.ts @@ -40,13 +40,19 @@ export class IntervalService { } } - stop() { + stop(): { isExecuting: boolean } { + const returnValue = { + isExecuting: this._isExecuting, + }; + if (!this._isEnabled) { - return; + return returnValue; } this._isEnabled = false; this.#clearNextInterval(); + + return returnValue; } resetCurrentInterval() { From c62cf108bee00d07c3336f006356993ef2efbc8a Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 1 May 2025 23:08:31 +0100 Subject: [PATCH 38/62] add supervisor to publish workflow --- .github/workflows/publish-worker-re2.yml | 16 ++++++++-------- .github/workflows/publish-worker.yml | 16 ++++++++-------- .github/workflows/publish.yml | 7 +++++++ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish-worker-re2.yml b/.github/workflows/publish-worker-re2.yml index 258af99bb7..bfe429593c 100644 --- a/.github/workflows/publish-worker-re2.yml +++ b/.github/workflows/publish-worker-re2.yml @@ -85,11 +85,11 @@ jobs: REPOSITORY: ${{ steps.get_repository.outputs.repo }} IMAGE_TAG: ${{ steps.get_tag.outputs.tag }} - - name: 🐙 Push 'v3' tag to GitHub Container Registry - if: steps.get_tag.outputs.is_semver == 'true' - run: | - docker tag infra_image "$REGISTRY/$REPOSITORY:v3" - docker push "$REGISTRY/$REPOSITORY:v3" - env: - REGISTRY: ghcr.io/triggerdotdev - REPOSITORY: ${{ steps.get_repository.outputs.repo }} + # - name: 🐙 Push 'v3' tag to GitHub Container Registry + # if: steps.get_tag.outputs.is_semver == 'true' + # run: | + # docker tag infra_image "$REGISTRY/$REPOSITORY:v3" + # docker push "$REGISTRY/$REPOSITORY:v3" + # env: + # REGISTRY: ghcr.io/triggerdotdev + # REPOSITORY: ${{ steps.get_repository.outputs.repo }} diff --git a/.github/workflows/publish-worker.yml b/.github/workflows/publish-worker.yml index 8c0d7ea3c2..74a70d8366 100644 --- a/.github/workflows/publish-worker.yml +++ b/.github/workflows/publish-worker.yml @@ -77,11 +77,11 @@ jobs: REPOSITORY: ${{ steps.get_repository.outputs.repo }} IMAGE_TAG: ${{ steps.get_tag.outputs.tag }} - - name: 🐙 Push 'v3' tag to GitHub Container Registry - if: steps.get_tag.outputs.is_semver == 'true' - run: | - docker tag infra_image "$REGISTRY/$REPOSITORY:v3" - docker push "$REGISTRY/$REPOSITORY:v3" - env: - REGISTRY: ghcr.io/triggerdotdev - REPOSITORY: ${{ steps.get_repository.outputs.repo }} + # - name: 🐙 Push 'v3' tag to GitHub Container Registry + # if: steps.get_tag.outputs.is_semver == 'true' + # run: | + # docker tag infra_image "$REGISTRY/$REPOSITORY:v3" + # docker push "$REGISTRY/$REPOSITORY:v3" + # env: + # REGISTRY: ghcr.io/triggerdotdev + # REPOSITORY: ${{ steps.get_repository.outputs.repo }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0f011c6cad..674b410eaa 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -68,3 +68,10 @@ jobs: secrets: inherit with: image_tag: ${{ inputs.image_tag }} + + publish-worker-v4: + needs: [typecheck, units] + uses: ./.github/workflows/publish-worker-v4.yml + secrets: inherit + with: + image_tag: ${{ inputs.image_tag }} From 467f9de097f858350eb7db8759228b37b9cb9b0f Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 2 May 2025 10:30:54 +0100 Subject: [PATCH 39/62] always print full deploy logs in CI --- .changeset/sweet-dolphins-invent.md | 5 ++ packages/cli-v3/src/commands/deploy.ts | 63 ++++++++++++++++++-------- 2 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 .changeset/sweet-dolphins-invent.md diff --git a/.changeset/sweet-dolphins-invent.md b/.changeset/sweet-dolphins-invent.md new file mode 100644 index 0000000000..df758a89e9 --- /dev/null +++ b/.changeset/sweet-dolphins-invent.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +Always print full deploy logs in CI diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 224cf7d1a9..a51b72c1d5 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -1,10 +1,11 @@ -import { intro, outro } from "@clack/prompts"; +import { intro, log, outro } from "@clack/prompts"; import { prepareDeploymentError } from "@trigger.dev/core/v3"; import { InitializeDeploymentResponseBody } from "@trigger.dev/core/v3/schemas"; import { Command, Option as CommandOption } from "commander"; import { resolve } from "node:path"; import { x } from "tinyexec"; import { z } from "zod"; +import { isCI } from "std-env"; import { CliApiClient } from "../apiClient.js"; import { buildWorker } from "../build/buildWorker.js"; import { resolveAlwaysExternal } from "../build/externals.js"; @@ -321,24 +322,24 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { const version = deployment.version; - const deploymentLink = cliLink( - "View deployment", - `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project}/deployments/${deployment.shortCode}` - ); + const rawDeploymentLink = `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project}/deployments/${deployment.shortCode}`; + const rawTestLink = `${authorization.dashboardUrl}/projects/v3/${ + resolvedConfig.project + }/test?environment=${options.env === "prod" ? "prod" : "stg"}`; - const testLink = cliLink( - "Test tasks", - `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project}/test?environment=${ - options.env === "prod" ? "prod" : "stg" - }` - ); + const deploymentLink = cliLink("View deployment", rawDeploymentLink); + const testLink = cliLink("Test tasks", rawTestLink); const $spinner = spinner(); - if (isLinksSupported) { - $spinner.start(`Building version ${version} ${deploymentLink}`); + if (isCI) { + log.step(`Building version ${version}\n`); } else { - $spinner.start(`Building version ${version}`); + if (isLinksSupported) { + $spinner.start(`Building version ${version} ${deploymentLink}`); + } else { + $spinner.start(`Building version ${version}`); + } } const selfHostedRegistryHost = deployment.registryHost ?? options.registry; @@ -368,6 +369,11 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { buildEnvVars: buildManifest.build.env, network: options.network, onLog: (logMessage) => { + if (isCI) { + console.log(logMessage); + return; + } + if (isLinksSupported) { $spinner.message(`Building version ${version} ${deploymentLink}: ${logMessage}`); } else { @@ -441,10 +447,14 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { }` : `${buildResult.image}${buildResult.digest ? `@${buildResult.digest}` : ""}`; - if (isLinksSupported) { - $spinner.message(`Deploying version ${version} ${deploymentLink}`); + if (isCI) { + log.step(`Deploying version ${version}\n`); } else { - $spinner.message(`Deploying version ${version}`); + if (isLinksSupported) { + $spinner.message(`Deploying version ${version} ${deploymentLink}`); + } else { + $spinner.message(`Deploying version ${version}`); + } } const finalizeResponse = await projectClient.client.finalizeDeployment( @@ -455,6 +465,11 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { skipPromotion: options.skipPromotion, }, (logMessage) => { + if (isCI) { + console.log(logMessage); + return; + } + if (isLinksSupported) { $spinner.message(`Deploying version ${version} ${deploymentLink}: ${logMessage}`); } else { @@ -475,7 +490,11 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { throw new SkipLoggingError("Failed to finalize deployment"); } - $spinner.stop(`Successfully deployed version ${version}`); + if (isCI) { + log.step(`Successfully deployed version ${version}`); + } else { + $spinner.stop(`Successfully deployed version ${version}`); + } const taskCount = deploymentWithWorker.worker?.tasks.length ?? 0; @@ -485,6 +504,14 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { }` ); + if (!isLinksSupported) { + console.log("View deployment"); + console.log(rawDeploymentLink); + console.log(); // new line + console.log("Test tasks"); + console.log(rawTestLink); + } + setGithubActionsOutputAndEnvVars({ envVars: { TRIGGER_DEPLOYMENT_VERSION: version, From 246c1a91e8c1c904de32ba361fc0059c08b99334 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 2 May 2025 10:47:31 +0100 Subject: [PATCH 40/62] Revert "avoid mutation during iteration when resolving pending waitpoints" This reverts commit 87b0ce1e5b63742af8019ad285fa6a5f868e5594. --- packages/core/src/v3/runtime/sharedRuntimeManager.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/core/src/v3/runtime/sharedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts index 7a881d4076..acf734c05d 100644 --- a/packages/core/src/v3/runtime/sharedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -259,14 +259,7 @@ export class SharedRuntimeManager implements RuntimeManager { } private resolvePendingWaitpoints(): void { - // Clone keys first to avoid mutation-during-iteration hazards - for (const resolverId of Array.from(this.waitpointsByResolverId.keys())) { - const waitpoint = this.waitpointsByResolverId.get(resolverId); - - if (!waitpoint) { - continue; - } - + for (const [resolverId, waitpoint] of this.waitpointsByResolverId.entries()) { this.resolveWaitpoint(waitpoint, resolverId); } } From 9403409637286a99b6df53bcf3759e5fe9f56ba0 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 2 May 2025 10:47:42 +0100 Subject: [PATCH 41/62] disable pre --- .changeset/pre.json | 47 --------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index 2cd89f46ab..0000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "mode": "pre", - "tag": "v4-beta", - "initialVersions": { - "coordinator": "0.0.1", - "docker-provider": "0.0.1", - "kubernetes-provider": "0.0.1", - "supervisor": "0.0.1", - "webapp": "1.0.0", - "@trigger.dev/build": "3.3.17", - "trigger.dev": "3.3.17", - "@trigger.dev/core": "3.3.17", - "@trigger.dev/python": "3.3.17", - "@trigger.dev/react-hooks": "3.3.17", - "@trigger.dev/redis-worker": "3.3.17", - "@trigger.dev/rsc": "3.3.17", - "@trigger.dev/sdk": "3.3.17" - }, - "changesets": [ - "blue-eyes-tickle", - "breezy-turtles-talk", - "eighty-rings-divide", - "four-needles-add", - "gentle-waves-suffer", - "green-lions-relate", - "hip-cups-wave", - "honest-files-decide", - "late-chairs-ring", - "moody-squids-count", - "nice-colts-boil", - "polite-impalas-care", - "polite-lies-fix", - "red-wasps-cover", - "shiny-kiwis-beam", - "smart-coins-hammer", - "sour-mirrors-accept", - "spotty-ducks-punch", - "spotty-pants-wink", - "tender-jobs-collect", - "tidy-books-smell", - "tiny-buckets-teach", - "tricky-houses-invite", - "two-tigers-dream", - "weak-jobs-hide", - "wet-deers-think" - ] -} From 4607b0558f044eb6655c265ea4060f2d9bc6734c Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 2 May 2025 10:50:23 +0100 Subject: [PATCH 42/62] print prerelease script errors --- scripts/publish-prerelease.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/publish-prerelease.sh b/scripts/publish-prerelease.sh index 763a80fbe0..66602fb069 100755 --- a/scripts/publish-prerelease.sh +++ b/scripts/publish-prerelease.sh @@ -55,7 +55,8 @@ if output=$(pnpm exec changeset version --snapshot $version 2>&1); then exit 0 fi else - echo "Error running changeset version command" + echo "$output" + echo "Error running changeset version command, detailed output above" exit 1 fi From ed1a44c76bb6818846effc7c7e72f9358d2c3024 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 2 May 2025 10:50:59 +0100 Subject: [PATCH 43/62] Revert "disable pre" This reverts commit 9403409637286a99b6df53bcf3759e5fe9f56ba0. --- .changeset/pre.json | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..2cd89f46ab --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,47 @@ +{ + "mode": "pre", + "tag": "v4-beta", + "initialVersions": { + "coordinator": "0.0.1", + "docker-provider": "0.0.1", + "kubernetes-provider": "0.0.1", + "supervisor": "0.0.1", + "webapp": "1.0.0", + "@trigger.dev/build": "3.3.17", + "trigger.dev": "3.3.17", + "@trigger.dev/core": "3.3.17", + "@trigger.dev/python": "3.3.17", + "@trigger.dev/react-hooks": "3.3.17", + "@trigger.dev/redis-worker": "3.3.17", + "@trigger.dev/rsc": "3.3.17", + "@trigger.dev/sdk": "3.3.17" + }, + "changesets": [ + "blue-eyes-tickle", + "breezy-turtles-talk", + "eighty-rings-divide", + "four-needles-add", + "gentle-waves-suffer", + "green-lions-relate", + "hip-cups-wave", + "honest-files-decide", + "late-chairs-ring", + "moody-squids-count", + "nice-colts-boil", + "polite-impalas-care", + "polite-lies-fix", + "red-wasps-cover", + "shiny-kiwis-beam", + "smart-coins-hammer", + "sour-mirrors-accept", + "spotty-ducks-punch", + "spotty-pants-wink", + "tender-jobs-collect", + "tidy-books-smell", + "tiny-buckets-teach", + "tricky-houses-invite", + "two-tigers-dream", + "weak-jobs-hide", + "wet-deers-think" + ] +} From 213a9832020373a6a5e4322c5842745da1c648ba Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 2 May 2025 12:55:45 +0100 Subject: [PATCH 44/62] misc fixes --- .../src/entryPoints/managed/controller.ts | 25 +-- .../src/entryPoints/managed/execution.ts | 149 ++++++++++-------- .../cli-v3/src/executions/taskRunProcess.ts | 36 +++-- 3 files changed, 120 insertions(+), 90 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/controller.ts b/packages/cli-v3/src/entryPoints/managed/controller.ts index 3c6bef8138..bc9f0b5493 100644 --- a/packages/cli-v3/src/entryPoints/managed/controller.ts +++ b/packages/cli-v3/src/entryPoints/managed/controller.ts @@ -384,7 +384,7 @@ export class ManagedRunController { properties: { code }, }); - this.currentExecution?.exit(); + this.currentExecution?.kill().catch(() => {}); process.exit(code); } @@ -581,16 +581,6 @@ export class ManagedRunController { return socket; } - async cancelAttempt(runId: string) { - this.sendDebugLog({ - runId, - message: "cancelling attempt", - properties: { runId }, - }); - - await this.currentExecution?.cancel(); - } - start() { this.sendDebugLog({ runId: this.runFriendlyId, @@ -619,7 +609,18 @@ export class ManagedRunController { message: "Shutting down", }); - await this.currentExecution?.cancel(); + // Cancel the current execution + const [error] = await tryCatch(this.currentExecution?.cancel()); + + if (error) { + this.sendDebugLog({ + runId: this.runFriendlyId, + message: "Error during shutdown", + properties: { error: String(error) }, + }); + } + + // Close the socket this.socket.close(); } diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index fae6206a9b..fb42773fd4 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -69,6 +69,7 @@ export class RunExecution { private lastHeartbeat?: Date; private isShuttingDown = false; + private shutdownReason?: string; constructor(opts: RunExecutionOptions) { this.id = randomBytes(4).toString("hex"); @@ -81,11 +82,39 @@ export class RunExecution { this.executionAbortController = new AbortController(); } + /** + * Cancels the current execution. + */ + public async cancel(): Promise { + if (this.isShuttingDown) { + throw new Error("cancel called after execution shut down"); + } + + this.sendDebugLog("cancelling attempt", { runId: this.runFriendlyId }); + + await this.taskRunProcess?.cancel(); + } + + /** + * Kills the current execution. + */ + public async kill({ exitExecution = true }: { exitExecution?: boolean } = {}) { + await this.taskRunProcess?.kill("SIGKILL"); + + if (exitExecution) { + this.shutdown("kill"); + } + } + /** * Prepares the execution with task run environment variables. * This should be called before executing, typically after a successful run to prepare for the next one. */ public prepareForExecution(opts: RunExecutionPrepareOptions): this { + if (this.isShuttingDown) { + throw new Error("prepareForExecution called after execution shut down"); + } + if (this.taskRunProcess) { throw new Error("prepareForExecution called after process was already created"); } @@ -204,7 +233,8 @@ export class RunExecution { if (this.currentAttemptNumber && this.currentAttemptNumber !== run.attemptNumber) { this.sendDebugLog("error: attempt number mismatch", snapshotMetadata); - await this.taskRunProcess?.suspend(); + // This is a rogue execution, a new one will already have been created elsewhere + await this.exitTaskRunProcessWithoutFailingRun({ flush: false }); return; } @@ -237,14 +267,14 @@ export class RunExecution { this.sendDebugLog("run was re-queued", snapshotMetadata); // Pretend we've just suspended the run. This will kill the process without failing the run. - await this.taskRunProcess?.suspend(); + await this.exitTaskRunProcessWithoutFailingRun({ flush: true }); return; } case "FINISHED": { this.sendDebugLog("run is finished", snapshotMetadata); // Pretend we've just suspended the run. This will kill the process without failing the run. - await this.taskRunProcess?.suspend(); + await this.exitTaskRunProcessWithoutFailingRun({ flush: true }); return; } case "QUEUED_EXECUTING": @@ -255,11 +285,11 @@ export class RunExecution { return; } case "SUSPENDED": { - this.sendDebugLog("run was suspended, kill the process", snapshotMetadata); + this.sendDebugLog("run was suspended", snapshotMetadata); // This will kill the process and fail the execution with a SuspendedProcessError - await this.taskRunProcess?.suspend(); - + // We don't flush because we already did before suspending + await this.exitTaskRunProcessWithoutFailingRun({ flush: false }); return; } case "PENDING_EXECUTING": { @@ -384,6 +414,10 @@ export class RunExecution { * When this returns, the child process will have been cleaned up. */ public async execute(runOpts: RunExecutionRunOptions): Promise { + if (this.isShuttingDown) { + throw new Error("execute called after execution shut down"); + } + // Setup initial state this.runFriendlyId = runOpts.runFriendlyId; @@ -420,7 +454,7 @@ export class RunExecution { if (startError) { this.sendDebugLog("failed to start attempt", { error: startError.message }); - this.stopServices(); + this.shutdown("failed to start attempt"); return; } @@ -429,11 +463,12 @@ export class RunExecution { if (executeError) { this.sendDebugLog("failed to execute run", { error: executeError.message }); - this.stopServices(); + this.shutdown("failed to execute run"); return; } - this.stopServices(); + // This is here for safety, but it + this.shutdown("execute call finished"); } private async executeRunWrapper({ @@ -460,10 +495,7 @@ export class RunExecution { }) ); - this.sendDebugLog("run execution completed", { error: executeError?.message }); - if (!executeError) { - this.stopServices(); return; } @@ -505,8 +537,6 @@ export class RunExecution { if (completeError) { this.sendDebugLog("failed to complete run", { error: completeError.message }); } - - this.stopServices(); } private async executeRun({ @@ -527,7 +557,7 @@ export class RunExecution { !this.taskRunProcess.isPreparedForNextAttempt ) { this.sendDebugLog("killing existing task run process before executing next attempt"); - await this.kill().catch(() => {}); + await this.kill({ exitExecution: false }).catch(() => {}); } // To skip this step and eagerly create the task run process, run prepareForExecution first @@ -578,25 +608,6 @@ export class RunExecution { } } - /** - * Cancels the current execution. - */ - public async cancel(): Promise { - this.sendDebugLog("cancelling attempt", { runId: this.runFriendlyId }); - - await this.taskRunProcess?.cancel(); - } - - public exit() { - if (this.taskRunProcess?.isPreparedForNextRun) { - this.taskRunProcess?.forceExit(); - } - } - - public async kill() { - await this.taskRunProcess?.kill("SIGKILL"); - } - private async complete({ completion }: { completion: TaskRunExecutionResult }): Promise { if (!this.runFriendlyId || !this.snapshotManager) { throw new Error("cannot complete run: missing run or snapshot manager"); @@ -639,37 +650,32 @@ export class RunExecution { const { attemptStatus } = result; - if (attemptStatus === "RUN_FINISHED") { - this.sendDebugLog("run finished"); - - return; - } - - if (attemptStatus === "RUN_PENDING_CANCEL") { - this.sendDebugLog("run pending cancel"); - return; - } + switch (attemptStatus) { + case "RUN_FINISHED": + case "RUN_PENDING_CANCEL": + case "RETRY_QUEUED": { + return; + } + case "RETRY_IMMEDIATELY": { + if (attemptStatus !== "RETRY_IMMEDIATELY") { + return; + } - if (attemptStatus === "RETRY_QUEUED") { - this.sendDebugLog("retry queued"); + if (completion.ok) { + throw new Error("Should retry but completion OK."); + } - return; - } + if (!completion.retry) { + throw new Error("Should retry but missing retry params."); + } - if (attemptStatus === "RETRY_IMMEDIATELY") { - if (completion.ok) { - throw new Error("Should retry but completion OK."); + await this.retryImmediately({ retryOpts: completion.retry }); + return; } - - if (!completion.retry) { - throw new Error("Should retry but missing retry params."); + default: { + assertExhaustive(attemptStatus); } - - await this.retryImmediately({ retryOpts: completion.retry }); - return; } - - assertExhaustive(attemptStatus); } private updateSnapshotAfterCompletion(snapshotId: string, status: TaskRunExecutionStatus) { @@ -752,7 +758,7 @@ export class RunExecution { if (startError) { this.sendDebugLog("failed to start attempt for retry", { error: startError.message }); - this.stopServices(); + this.shutdown("retryImmediately: failed to start attempt"); return; } @@ -761,11 +767,9 @@ export class RunExecution { if (executeError) { this.sendDebugLog("failed to execute run for retry", { error: executeError.message }); - this.stopServices(); + this.shutdown("retryImmediately: failed to execute run"); return; } - - this.stopServices(); } /** @@ -797,10 +801,17 @@ export class RunExecution { this.restoreCount++; } + private async exitTaskRunProcessWithoutFailingRun({ flush }: { flush: boolean }) { + await this.taskRunProcess?.suspend({ flush }); + + // No services should be left running after this line - let's make sure of it + this.shutdown("exitTaskRunProcessWithoutFailingRun"); + } + /** * Processes env overrides from the metadata service. Generally called when we're resuming from a suspended state. */ - async processEnvOverrides(reason?: string) { + public async processEnvOverrides(reason?: string) { if (!this.env.TRIGGER_METADATA_URL) { this.sendDebugLog("no metadata url, skipping env overrides", { reason }); return; @@ -953,15 +964,21 @@ export class RunExecution { } this.executionAbortController.abort(); - this.stopServices(); + this.shutdown("abortExecution"); } - private stopServices() { + private shutdown(reason: string) { if (this.isShuttingDown) { + this.sendDebugLog(`[shutdown] ${reason} (already shutting down)`, { + firstShutdownReason: this.shutdownReason, + }); return; } + this.sendDebugLog(`[shutdown] ${reason}`); + this.isShuttingDown = true; + this.shutdownReason = reason; this.snapshotPoller?.stop(); this.snapshotManager?.cleanup(); diff --git a/packages/cli-v3/src/executions/taskRunProcess.ts b/packages/cli-v3/src/executions/taskRunProcess.ts index 6ac384cc03..9b8c217fbb 100644 --- a/packages/cli-v3/src/executions/taskRunProcess.ts +++ b/packages/cli-v3/src/executions/taskRunProcess.ts @@ -9,6 +9,7 @@ import { TaskRunExecutionPayload, TaskRunExecutionResult, type TaskRunInternalError, + tryCatch, WorkerManifest, WorkerToExecutorMessageCatalog, } from "@trigger.dev/core/v3"; @@ -102,6 +103,7 @@ export class TaskRunProcess { this.onIsBeingKilled.detach(); this.onSendDebugLog.detach(); this.onSetSuspendable.detach(); + this.onTaskRunHeartbeat.detach(); } async cancel() { @@ -373,6 +375,7 @@ export class TaskRunProcess { this._stderr.push(errorLine); } + /** This will never throw. */ async kill(signal?: number | NodeJS.Signals, timeoutInMs?: number) { logger.debug(`killing task run process`, { signal, @@ -386,26 +389,35 @@ export class TaskRunProcess { this.onIsBeingKilled.post(this); - this._child?.kill(signal); + try { + this._child?.kill(signal); + } catch (error) { + logger.debug("kill: failed to kill child process", { error }); + } + + if (!timeoutInMs) { + return; + } + + const [error] = await tryCatch(killTimeout); - if (timeoutInMs) { - await killTimeout; + if (error) { + logger.debug("kill: failed to wait for child process to exit", { error }); } } - async suspend() { + async suspend({ flush }: { flush: boolean }) { this._isBeingSuspended = true; - await this.kill("SIGKILL"); - } - forceExit() { - try { - this._isBeingKilled = true; + if (flush) { + const [error] = await tryCatch(this.#flush()); - this._child?.kill("SIGKILL"); - } catch (error) { - logger.debug("forceExit: failed to kill child process", { error }); + if (error) { + console.error("Error flushing task run process", { error }); + } } + + await this.kill("SIGKILL"); } get isBeingKilled() { From 344e2e5a4494ac69b170a8953f59576a2f63def6 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 2 May 2025 14:59:16 +0100 Subject: [PATCH 45/62] better debug logs --- .../cli-v3/src/entryPoints/managed/execution.ts | 10 ++++------ packages/cli-v3/src/entryPoints/managed/poller.ts | 14 +++++++++----- .../src/entryPoints/managed/snapshot.test.ts | 4 ++-- .../cli-v3/src/entryPoints/managed/snapshot.ts | 6 ++++-- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index fb42773fd4..d0726d7266 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -443,9 +443,7 @@ export class RunExecution { logger: this.logger, snapshotPollIntervalSeconds: this.env.TRIGGER_SNAPSHOT_POLL_INTERVAL_SECONDS, handleSnapshotChange: this.enqueueSnapshotChangeAndWait.bind(this), - }); - - this.snapshotPoller.start(); + }).start(); const [startError, start] = await tryCatch( this.startAttempt({ isWarmStart: runOpts.isWarmStart }) @@ -500,7 +498,7 @@ export class RunExecution { } if (executeError instanceof SuspendedProcessError) { - this.sendDebugLog("run was suspended", { + this.sendDebugLog("execution was suspended", { run: run.friendlyId, snapshot: snapshot.friendlyId, error: executeError.message, @@ -510,7 +508,7 @@ export class RunExecution { } if (executeError instanceof ExecutionAbortError) { - this.sendDebugLog("run was interrupted", { + this.sendDebugLog("execution was aborted", { run: run.friendlyId, snapshot: snapshot.friendlyId, error: executeError.message, @@ -981,7 +979,7 @@ export class RunExecution { this.shutdownReason = reason; this.snapshotPoller?.stop(); - this.snapshotManager?.cleanup(); + this.snapshotManager?.dispose(); this.taskRunProcess?.unsafeDetachEvtHandlers(); } diff --git a/packages/cli-v3/src/entryPoints/managed/poller.ts b/packages/cli-v3/src/entryPoints/managed/poller.ts index 31b04be4ce..fb4c5fb8f8 100644 --- a/packages/cli-v3/src/entryPoints/managed/poller.ts +++ b/packages/cli-v3/src/entryPoints/managed/poller.ts @@ -63,8 +63,6 @@ export class RunExecutionSnapshotPoller { }); }, }); - - this.sendDebugLog("created"); } private sendDebugLog(message: string, properties?: SendDebugLogOptions["properties"]) { @@ -93,14 +91,18 @@ export class RunExecutionSnapshotPoller { this.poller.updateInterval(intervalMs); } - start() { + start(): RunExecutionSnapshotPoller { if (this.enabled) { this.sendDebugLog("already started"); - return; + return this; } + this.sendDebugLog("start"); + this.enabled = true; this.poller.start(); + + return this; } stop() { @@ -109,12 +111,14 @@ export class RunExecutionSnapshotPoller { return; } + this.sendDebugLog("stop"); + this.enabled = false; const { isExecuting } = this.poller.stop(); if (isExecuting) { - this.sendDebugLog("stopped poller but it's still executing"); + this.sendDebugLog("stopped while executing"); } } } diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts index 256db83cfe..1efd0de6ca 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts @@ -306,8 +306,8 @@ describe("SnapshotManager", () => { expect(manager.queueLength).not.toBe(0); - // Call cleanup before they complete - manager.cleanup(); + // Dispose manager before any promises complete + manager.dispose(); expect(manager.queueLength).toBe(0); diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.ts index ce8b5ad3e4..f109db17d0 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.ts @@ -286,10 +286,12 @@ export class SnapshotManager { } } - public cleanup() { + public dispose() { + this.sendDebugLog("dispose"); + // Clear any pending changes for (const item of this.changeQueue) { - item.reject(new Error("SnapshotManager cleanup")); + item.reject(new Error("SnapshotManager disposed")); } this.changeQueue = []; } From b4c61af903a2dbc525da4d5e3e761459b62b9aa7 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 10:08:12 +0100 Subject: [PATCH 46/62] add snapshots since methods and route --- ...nFriendlyId.snapshots.since.$snapshotId.ts | 30 ++++ .../worker/workerGroupTokenService.server.ts | 13 ++ .../run-engine/src/engine/index.ts | 61 +++----- .../engine/systems/executionSnapshotSystem.ts | 117 +++++++++++--- .../src/engine/tests/waitpoints.test.ts | 144 ++++++++++++++++++ .../src/entryPoints/managed/controller.ts | 10 +- .../v3/runEngineWorker/supervisor/schemas.ts | 7 + .../src/v3/runEngineWorker/workload/http.ts | 14 ++ .../v3/runEngineWorker/workload/schemas.ts | 6 + 9 files changed, 345 insertions(+), 57 deletions(-) create mode 100644 apps/webapp/app/routes/engine.v1.worker-actions.runs.$runFriendlyId.snapshots.since.$snapshotId.ts diff --git a/apps/webapp/app/routes/engine.v1.worker-actions.runs.$runFriendlyId.snapshots.since.$snapshotId.ts b/apps/webapp/app/routes/engine.v1.worker-actions.runs.$runFriendlyId.snapshots.since.$snapshotId.ts new file mode 100644 index 0000000000..1531205c24 --- /dev/null +++ b/apps/webapp/app/routes/engine.v1.worker-actions.runs.$runFriendlyId.snapshots.since.$snapshotId.ts @@ -0,0 +1,30 @@ +import { json, TypedResponse } from "@remix-run/server-runtime"; +import { WorkerApiRunSnapshotsSinceResponseBody } from "@trigger.dev/core/v3/workers"; +import { z } from "zod"; +import { createLoaderWorkerApiRoute } from "~/services/routeBuilders/apiBuilder.server"; + +export const loader = createLoaderWorkerApiRoute( + { + params: z.object({ + runFriendlyId: z.string(), + snapshotId: z.string(), + }), + }, + async ({ + authenticatedWorker, + params, + }): Promise> => { + const { runFriendlyId, snapshotId } = params; + + const executions = await authenticatedWorker.getSnapshotsSince({ + runFriendlyId, + snapshotId, + }); + + if (!executions) { + throw new Error("Failed to retrieve snapshots since given snapshot"); + } + + return json({ executions }); + } +); diff --git a/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts b/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts index d7a8a10d7d..5c3a8d1cce 100644 --- a/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts +++ b/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts @@ -759,6 +759,19 @@ export class AuthenticatedWorkerInstance extends WithRunEngine { }); } + async getSnapshotsSince({ + runFriendlyId, + snapshotId, + }: { + runFriendlyId: string; + snapshotId: string; + }) { + return await this._engine.getSnapshotsSince({ + runId: fromFriendlyId(runFriendlyId), + snapshotId: fromFriendlyId(snapshotId), + }); + } + toJSON(): WorkerGroupTokenAuthenticationResponse { if (this.type === WorkerInstanceGroupType.MANAGED) { return { diff --git a/internal-packages/run-engine/src/engine/index.ts b/internal-packages/run-engine/src/engine/index.ts index f1102a195c..9361a4d366 100644 --- a/internal-packages/run-engine/src/engine/index.ts +++ b/internal-packages/run-engine/src/engine/index.ts @@ -43,6 +43,8 @@ import { EnqueueSystem } from "./systems/enqueueSystem.js"; import { ExecutionSnapshotSystem, getLatestExecutionSnapshot, + getExecutionSnapshotsSince, + executionDataFromSnapshot, } from "./systems/executionSnapshotSystem.js"; import { PendingVersionSystem } from "./systems/pendingVersionSystem.js"; import { ReleaseConcurrencySystem } from "./systems/releaseConcurrencySystem.js"; @@ -1100,43 +1102,31 @@ export class RunEngine { const prisma = tx ?? this.prisma; try { const snapshot = await getLatestExecutionSnapshot(prisma, runId); + return executionDataFromSnapshot(snapshot); + } catch (e) { + this.logger.error("Failed to getRunExecutionData", { + message: e instanceof Error ? e.message : e, + }); + return null; + } + } - const executionData: RunExecutionData = { - version: "1" as const, - snapshot: { - id: snapshot.id, - friendlyId: snapshot.friendlyId, - executionStatus: snapshot.executionStatus, - description: snapshot.description, - }, - run: { - id: snapshot.runId, - friendlyId: snapshot.runFriendlyId, - status: snapshot.runStatus, - attemptNumber: snapshot.attemptNumber ?? undefined, - }, - batch: snapshot.batchId - ? { - id: snapshot.batchId, - friendlyId: BatchId.toFriendlyId(snapshot.batchId), - } - : undefined, - checkpoint: snapshot.checkpoint - ? { - id: snapshot.checkpoint.id, - friendlyId: snapshot.checkpoint.friendlyId, - type: snapshot.checkpoint.type, - location: snapshot.checkpoint.location, - imageRef: snapshot.checkpoint.imageRef, - reason: snapshot.checkpoint.reason ?? undefined, - } - : undefined, - completedWaitpoints: snapshot.completedWaitpoints, - }; + async getSnapshotsSince({ + runId, + snapshotId, + tx, + }: { + runId: string; + snapshotId: string; + tx?: PrismaClientOrTransaction; + }): Promise { + const prisma = tx ?? this.prisma; - return executionData; + try { + const snapshots = await getExecutionSnapshotsSince(prisma, runId, snapshotId); + return snapshots.map(executionDataFromSnapshot); } catch (e) { - this.logger.error("Failed to getRunExecutionData", { + this.logger.error("Failed to getSnapshotsSince", { message: e instanceof Error ? e.message : e, }); return null; @@ -1158,9 +1148,6 @@ export class RunEngine { } } - //#endregion - - //#region Heartbeat async #handleStalledSnapshot({ runId, snapshotId, diff --git a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts index 25320697b0..52b354411c 100644 --- a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts @@ -1,4 +1,4 @@ -import { CompletedWaitpoint, ExecutionResult } from "@trigger.dev/core/v3"; +import { CompletedWaitpoint, ExecutionResult, RunExecutionData } from "@trigger.dev/core/v3"; import { BatchId, RunId, SnapshotId } from "@trigger.dev/core/v3/isomorphic"; import { Prisma, @@ -17,31 +17,23 @@ export type ExecutionSnapshotSystemOptions = { heartbeatTimeouts: HeartbeatTimeouts; }; -export interface LatestExecutionSnapshot extends TaskRunExecutionSnapshot { +export interface EnhancedExecutionSnapshot extends TaskRunExecutionSnapshot { friendlyId: string; runFriendlyId: string; checkpoint: TaskRunCheckpoint | null; completedWaitpoints: CompletedWaitpoint[]; } -/* Gets the most recent valid snapshot for a run */ -export async function getLatestExecutionSnapshot( - prisma: PrismaClientOrTransaction, - runId: string -): Promise { - const snapshot = await prisma.taskRunExecutionSnapshot.findFirst({ - where: { runId, isValid: true }, - include: { - completedWaitpoints: true, - checkpoint: true, - }, - orderBy: { createdAt: "desc" }, - }); - - if (!snapshot) { - throw new Error(`No execution snapshot found for TaskRun ${runId}`); - } +type ExecutionSnapshotWithCheckAndWaitpoints = Prisma.TaskRunExecutionSnapshotGetPayload<{ + include: { + checkpoint: true; + completedWaitpoints: true; + }; +}>; +function enhanceExecutionSnapshot( + snapshot: ExecutionSnapshotWithCheckAndWaitpoints +): EnhancedExecutionSnapshot { return { ...snapshot, friendlyId: SnapshotId.toFriendlyId(snapshot.id), @@ -99,6 +91,27 @@ export async function getLatestExecutionSnapshot( }; } +/* Gets the most recent valid snapshot for a run */ +export async function getLatestExecutionSnapshot( + prisma: PrismaClientOrTransaction, + runId: string +): Promise { + const snapshot = await prisma.taskRunExecutionSnapshot.findFirst({ + where: { runId, isValid: true }, + include: { + completedWaitpoints: true, + checkpoint: true, + }, + orderBy: { createdAt: "desc" }, + }); + + if (!snapshot) { + throw new Error(`No execution snapshot found for TaskRun ${runId}`); + } + + return enhanceExecutionSnapshot(snapshot); +} + export async function getExecutionSnapshotCompletedWaitpoints( prisma: PrismaClientOrTransaction, snapshotId: string @@ -141,6 +154,72 @@ export function executionResultFromSnapshot(snapshot: TaskRunExecutionSnapshot): }; } +export function executionDataFromSnapshot(snapshot: EnhancedExecutionSnapshot): RunExecutionData { + return { + version: "1" as const, + snapshot: { + id: snapshot.id, + friendlyId: snapshot.friendlyId, + executionStatus: snapshot.executionStatus, + description: snapshot.description, + }, + run: { + id: snapshot.runId, + friendlyId: snapshot.runFriendlyId, + status: snapshot.runStatus, + attemptNumber: snapshot.attemptNumber ?? undefined, + }, + batch: snapshot.batchId + ? { + id: snapshot.batchId, + friendlyId: BatchId.toFriendlyId(snapshot.batchId), + } + : undefined, + checkpoint: snapshot.checkpoint + ? { + id: snapshot.checkpoint.id, + friendlyId: snapshot.checkpoint.friendlyId, + type: snapshot.checkpoint.type, + location: snapshot.checkpoint.location, + imageRef: snapshot.checkpoint.imageRef, + reason: snapshot.checkpoint.reason ?? undefined, + } + : undefined, + completedWaitpoints: snapshot.completedWaitpoints, + }; +} + +export async function getExecutionSnapshotsSince( + prisma: PrismaClientOrTransaction, + runId: string, + sinceSnapshotId: string +): Promise { + // Find the createdAt of the sinceSnapshotId + const sinceSnapshot = await prisma.taskRunExecutionSnapshot.findUnique({ + where: { id: sinceSnapshotId }, + select: { createdAt: true }, + }); + + if (!sinceSnapshot) { + throw new Error(`No execution snapshot found for id ${sinceSnapshotId}`); + } + + const snapshots = await prisma.taskRunExecutionSnapshot.findMany({ + where: { + runId, + isValid: true, + createdAt: { gt: sinceSnapshot.createdAt }, + }, + include: { + completedWaitpoints: true, + checkpoint: true, + }, + orderBy: { createdAt: "asc" }, + }); + + return snapshots.map(enhanceExecutionSnapshot); +} + export class ExecutionSnapshotSystem { private readonly $: SystemResources; private readonly heartbeatTimeouts: HeartbeatTimeouts; diff --git a/internal-packages/run-engine/src/engine/tests/waitpoints.test.ts b/internal-packages/run-engine/src/engine/tests/waitpoints.test.ts index 3e4ae20afa..4beadd6a74 100644 --- a/internal-packages/run-engine/src/engine/tests/waitpoints.test.ts +++ b/internal-packages/run-engine/src/engine/tests/waitpoints.test.ts @@ -1344,4 +1344,148 @@ describe("RunEngine Waitpoints", () => { } } ); + + containerTest( + "getSnapshotsSince returns correct snapshots and handles errors", + async ({ prisma, redisOptions }) => { + //create environment + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", + machines: { + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, + }, + }, + baseCostInCents: 0.0001, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); + + try { + const taskIdentifier = "test-task"; + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + const run = await engine.trigger( + { + number: 1, + friendlyId: "run_snapshotsince", + environment: authenticatedEnvironment, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t_snapshotsince", + spanId: "s_snapshotsince", + masterQueue: "main", + queue: "task/test-task", + isTest: false, + tags: [], + }, + prisma + ); + + // Dequeue and start the run (snapshot 1) + const dequeued = await engine.dequeueFromMasterQueue({ + consumerId: "test_snapshotsince", + masterQueue: run.masterQueue, + maxRunCount: 10, + }); + const attemptResult = await engine.startRunAttempt({ + runId: dequeued[0].run.id, + snapshotId: dequeued[0].snapshot.id, + }); + + // Block the run with a waitpoint (snapshot 2) + const { waitpoint } = await engine.createDateTimeWaitpoint({ + projectId: authenticatedEnvironment.project.id, + environmentId: authenticatedEnvironment.id, + completedAfter: new Date(Date.now() + 100), + }); + await engine.blockRunWithWaitpoint({ + runId: run.id, + waitpoints: [waitpoint.id], + projectId: authenticatedEnvironment.project.id, + organizationId: authenticatedEnvironment.organization.id, + releaseConcurrency: true, + }); + + // Wait for the waitpoint to complete and unblock (snapshot 3) + await setTimeout(200); + await engine.completeWaitpoint({ id: waitpoint.id }); + await setTimeout(200); + + // Get all snapshots for the run + const allSnapshots = await prisma.taskRunExecutionSnapshot.findMany({ + where: { runId: run.id, isValid: true }, + orderBy: { createdAt: "asc" }, + }); + expect(allSnapshots.length).toBeGreaterThanOrEqual(3); + + // getSnapshotsSince with the first snapshot should return at least 2 + const sinceFirst = await engine.getSnapshotsSince({ + runId: run.id, + snapshotId: allSnapshots[0].id, + }); + assertNonNullable(sinceFirst); + expect(sinceFirst.length).toBeGreaterThanOrEqual(2); + + // Check completedWaitpoints for each returned snapshot + for (const snap of sinceFirst) { + expect(Array.isArray(snap.completedWaitpoints)).toBe(true); + } + + // At least one snapshot should have a completed waitpoint + expect(sinceFirst.some((snap) => snap.completedWaitpoints.length === 1)).toBe(true); + + // If any completedWaitpoints exist, check output is not an error + const withCompleted = sinceFirst.find((snap) => snap.completedWaitpoints.length === 1); + if (withCompleted) { + expect(withCompleted.completedWaitpoints[0].outputIsError).toBe(false); + } + + // getSnapshotsSince with the latest snapshot should return 0 + const sinceLatest = await engine.getSnapshotsSince({ + runId: run.id, + snapshotId: allSnapshots[allSnapshots.length - 1].id, + }); + assertNonNullable(sinceLatest); + expect(sinceLatest.length).toBe(0); + + // getSnapshotsSince with an invalid snapshotId should throw or return [] + let threw = false; + try { + const sinceInvalid = await engine.getSnapshotsSince({ + runId: run.id, + snapshotId: "invalid-id", + }); + expect(sinceInvalid).toBeNull(); + } catch (e) { + threw = true; + } + // should never throw + expect(threw).toBe(false); + } finally { + await engine.quit(); + } + } + ); }); diff --git a/packages/cli-v3/src/entryPoints/managed/controller.ts b/packages/cli-v3/src/entryPoints/managed/controller.ts index bc9f0b5493..90ebfd4dc8 100644 --- a/packages/cli-v3/src/entryPoints/managed/controller.ts +++ b/packages/cli-v3/src/entryPoints/managed/controller.ts @@ -429,7 +429,7 @@ export class ManagedRunController { if (!controller.runFriendlyId) { this.sendDebugLog({ runId: run.friendlyId, - message: "run:notify: ignoring notification, no local run ID", + message: "run:notify: ignoring notification, no run ID", properties: { notification, controller, @@ -438,6 +438,14 @@ export class ManagedRunController { return; } + if (!controller.snapshotFriendlyId) { + this.sendDebugLog({ + runId: run.friendlyId, + message: "run:notify: ignoring notification, no snapshot ID", + properties: { notification, controller }, + }); + } + if (run.friendlyId !== controller.runFriendlyId) { this.sendDebugLog({ runId: run.friendlyId, diff --git a/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts b/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts index 1e5025eff3..deeaadf47d 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts @@ -162,3 +162,10 @@ export const WorkerApiSuspendCompletionResponseBody = z.object({ export type WorkerApiSuspendCompletionResponseBody = z.infer< typeof WorkerApiSuspendCompletionResponseBody >; + +export const WorkerApiRunSnapshotsSinceResponseBody = z.object({ + executions: z.array(RunExecutionData), +}); +export type WorkerApiRunSnapshotsSinceResponseBody = z.infer< + typeof WorkerApiRunSnapshotsSinceResponseBody +>; diff --git a/packages/core/src/v3/runEngineWorker/workload/http.ts b/packages/core/src/v3/runEngineWorker/workload/http.ts index 9dde07d35d..22978be81b 100644 --- a/packages/core/src/v3/runEngineWorker/workload/http.ts +++ b/packages/core/src/v3/runEngineWorker/workload/http.ts @@ -11,6 +11,7 @@ import { WorkloadSuspendRunResponseBody, WorkloadContinueRunExecutionResponseBody, WorkloadDebugLogRequestBody, + WorkloadRunSnapshotsSinceResponseBody, } from "./schemas.js"; import { WorkloadClientCommonOptions } from "./types.js"; import { getDefaultWorkloadHeaders } from "./util.js"; @@ -142,6 +143,19 @@ export class WorkloadHttpClient { ); } + async getSnapshotsSince(runId: string, snapshotId: string) { + return wrapZodFetch( + WorkloadRunSnapshotsSinceResponseBody, + `${this.apiUrl}/api/v1/workload-actions/runs/${runId}/snapshots/since/${snapshotId}`, + { + method: "GET", + headers: { + ...this.defaultHeaders(), + }, + } + ); + } + async sendDebugLog(runId: string, body: WorkloadDebugLogRequestBody): Promise { try { const res = await wrapZodFetch( diff --git a/packages/core/src/v3/runEngineWorker/workload/schemas.ts b/packages/core/src/v3/runEngineWorker/workload/schemas.ts index 93c4252d37..14f3b8efe3 100644 --- a/packages/core/src/v3/runEngineWorker/workload/schemas.ts +++ b/packages/core/src/v3/runEngineWorker/workload/schemas.ts @@ -10,6 +10,7 @@ import { WorkerApiDequeueFromVersionResponseBody, WorkerApiContinueRunExecutionRequestBody, WorkerApiDebugLogBody, + WorkerApiRunSnapshotsSinceResponseBody, } from "../supervisor/schemas.js"; export const WorkloadHeartbeatRequestBody = WorkerApiRunHeartbeatRequestBody; @@ -64,3 +65,8 @@ export const WorkloadDequeueFromVersionResponseBody = WorkerApiDequeueFromVersio export type WorkloadDequeueFromVersionResponseBody = z.infer< typeof WorkloadDequeueFromVersionResponseBody >; + +export const WorkloadRunSnapshotsSinceResponseBody = WorkerApiRunSnapshotsSinceResponseBody; +export type WorkloadRunSnapshotsSinceResponseBody = z.infer< + typeof WorkloadRunSnapshotsSinceResponseBody +>; From a7e4ddd409e1482b404cb066e7427a76654e6750 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 13:50:14 +0100 Subject: [PATCH 47/62] prep for snapshots since --- .../src/entryPoints/managed/controller.ts | 122 ++---------------- .../src/entryPoints/managed/execution.ts | 62 ++++++++- .../src/entryPoints/managed/notifier.ts | 89 +++++++++++++ .../cli-v3/src/entryPoints/managed/poller.ts | 65 +++++----- 4 files changed, 194 insertions(+), 144 deletions(-) create mode 100644 packages/cli-v3/src/entryPoints/managed/notifier.ts diff --git a/packages/cli-v3/src/entryPoints/managed/controller.ts b/packages/cli-v3/src/entryPoints/managed/controller.ts index 90ebfd4dc8..a79b670354 100644 --- a/packages/cli-v3/src/entryPoints/managed/controller.ts +++ b/packages/cli-v3/src/entryPoints/managed/controller.ts @@ -18,7 +18,7 @@ type ManagedRunControllerOptions = { env: EnvObject; }; -type SupervisorSocket = Socket; +export type SupervisorSocket = Socket; export class ManagedRunController { private readonly env: RunnerEnv; @@ -31,6 +31,9 @@ export class ManagedRunController { private warmStartCount = 0; private restoreCount = 0; + private notificationCount = 0; + private lastNotificationAt: Date | null = null; + private currentExecution: RunExecution | null = null; constructor(opts: ManagedRunControllerOptions) { @@ -91,6 +94,8 @@ export class ManagedRunController { return { warmStartCount: this.warmStartCount, restoreCount: this.restoreCount, + notificationCount: this.notificationCount, + lastNotificationAt: this.lastNotificationAt, }; } @@ -188,12 +193,16 @@ export class ManagedRunController { this.currentExecution = null; } + // Remove all run notification listeners just to be safe + this.socket.removeAllListeners("run:notify"); + if (!this.currentExecution || !this.currentExecution.canExecute) { this.currentExecution = new RunExecution({ workerManifest: this.workerManifest, env: this.env, httpClient: this.httpClient, logger: this.logger, + supervisorSocket: this.socket, }); } @@ -224,8 +233,8 @@ export class ManagedRunController { const metrics = this.currentExecution?.metrics; - if (metrics?.restoreCount) { - this.restoreCount += metrics.restoreCount; + if (metrics?.execution?.restoreCount) { + this.restoreCount += metrics.execution.restoreCount; } this.lockedRunExecution = null; @@ -288,6 +297,7 @@ export class ManagedRunController { env: this.env, httpClient: this.httpClient, logger: this.logger, + supervisorSocket: this.socket, }).prepareForExecution({ taskRunEnv: previousTaskRunEnv, }); @@ -392,116 +402,12 @@ export class ManagedRunController { createSupervisorSocket(): SupervisorSocket { const wsUrl = new URL("/workload", this.workerApiUrl); - const socket = io(wsUrl.href, { + const socket: SupervisorSocket = io(wsUrl.href, { transports: ["websocket"], extraHeaders: { [WORKLOAD_HEADERS.DEPLOYMENT_ID]: this.env.TRIGGER_DEPLOYMENT_ID, [WORKLOAD_HEADERS.RUNNER_ID]: this.env.TRIGGER_RUNNER_ID, }, - }) satisfies SupervisorSocket; - - socket.on("run:notify", async ({ version, run }) => { - // Generate a unique ID for the notification - const notificationId = Math.random().toString(36).substring(2, 15); - - // Use this to track the notification incl. any processing - const notification = { - id: notificationId, - runId: run.friendlyId, - version, - }; - - // Lock this to the current run and snapshot IDs - const controller = { - runFriendlyId: this.runFriendlyId, - snapshotFriendlyId: this.snapshotFriendlyId, - }; - - this.sendDebugLog({ - runId: run.friendlyId, - message: "run:notify received by runner", - properties: { - notification, - controller, - }, - }); - - if (!controller.runFriendlyId) { - this.sendDebugLog({ - runId: run.friendlyId, - message: "run:notify: ignoring notification, no run ID", - properties: { - notification, - controller, - }, - }); - return; - } - - if (!controller.snapshotFriendlyId) { - this.sendDebugLog({ - runId: run.friendlyId, - message: "run:notify: ignoring notification, no snapshot ID", - properties: { notification, controller }, - }); - } - - if (run.friendlyId !== controller.runFriendlyId) { - this.sendDebugLog({ - runId: run.friendlyId, - message: "run:notify: ignoring notification for different run", - properties: { - notification, - controller, - }, - }); - return; - } - - const latestSnapshot = await this.httpClient.getRunExecutionData(controller.runFriendlyId); - - if (!latestSnapshot.success) { - this.sendDebugLog({ - runId: run.friendlyId, - message: "run:notify: failed to get latest snapshot data", - properties: { - notification, - controller, - error: latestSnapshot.error, - }, - }); - return; - } - - const runExecutionData = latestSnapshot.data.execution; - - if (!this.currentExecution) { - this.sendDebugLog({ - runId: run.friendlyId, - message: "run:notify: no current execution", - properties: { - notification, - controller, - }, - }); - return; - } - - const [error] = await tryCatch( - this.currentExecution.enqueueSnapshotChangeAndWait(runExecutionData) - ); - - if (error) { - this.sendDebugLog({ - runId: run.friendlyId, - message: "run:notify: unexpected error", - properties: { - notification, - controller, - error: error.message, - }, - }); - } }); socket.on("connect", () => { diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index d0726d7266..fca29c85e5 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -20,6 +20,8 @@ import { assertExhaustive, tryCatch } from "@trigger.dev/core/utils"; import { MetadataClient } from "./overrides.js"; import { randomBytes } from "node:crypto"; import { SnapshotManager, SnapshotState } from "./snapshot.js"; +import type { SupervisorSocket } from "./controller.js"; +import { RunNotifier } from "./notifier.js"; class ExecutionAbortError extends Error { constructor(message: string) { @@ -33,6 +35,7 @@ type RunExecutionOptions = { env: RunnerEnv; httpClient: WorkloadHttpClient; logger: RunLogger; + supervisorSocket: SupervisorSocket; }; type RunExecutionPrepareOptions = { @@ -71,12 +74,16 @@ export class RunExecution { private isShuttingDown = false; private shutdownReason?: string; + private supervisorSocket: SupervisorSocket; + private notifier?: RunNotifier; + constructor(opts: RunExecutionOptions) { this.id = randomBytes(4).toString("hex"); this.workerManifest = opts.workerManifest; this.env = opts.env; this.httpClient = opts.httpClient; this.logger = opts.logger; + this.supervisorSocket = opts.supervisorSocket; this.restoreCount = 0; this.executionAbortController = new AbortController(); @@ -439,10 +446,16 @@ export class RunExecution { this.snapshotPoller = new RunExecutionSnapshotPoller({ runFriendlyId: this.runFriendlyId, snapshotFriendlyId: this.snapshotManager.snapshotId, - httpClient: this.httpClient, logger: this.logger, snapshotPollIntervalSeconds: this.env.TRIGGER_SNAPSHOT_POLL_INTERVAL_SECONDS, - handleSnapshotChange: this.enqueueSnapshotChangeAndWait.bind(this), + onPoll: this.fetchAndEnqueueSnapshotChange.bind(this), + }).start(); + + this.notifier = new RunNotifier({ + runFriendlyId: this.runFriendlyId, + supervisorSocket: this.supervisorSocket, + onNotify: this.fetchAndEnqueueSnapshotChange.bind(this), + logger: this.logger, }).start(); const [startError, start] = await tryCatch( @@ -947,7 +960,12 @@ export class RunExecution { public get metrics() { return { - restoreCount: this.restoreCount, + execution: { + restoreCount: this.restoreCount, + lastHeartbeat: this.lastHeartbeat, + }, + poller: this.snapshotPoller?.metrics, + notifier: this.notifier?.metrics, }; } @@ -981,6 +999,8 @@ export class RunExecution { this.snapshotPoller?.stop(); this.snapshotManager?.dispose(); + this.notifier?.stop(); + this.taskRunProcess?.unsafeDetachEvtHandlers(); } @@ -1056,4 +1076,40 @@ export class RunExecution { this.sendDebugLog("suspending, any day now 🚬", { suspendableSnapshot }); } + + /** + * Fetches the latest execution data and enqueues snapshot changes. Used by both poller and notification handlers. + * @param source string - where this call originated (e.g. 'poller', 'notification') + */ + public async fetchAndEnqueueSnapshotChange(source: string): Promise { + if (!this.runFriendlyId) { + this.sendDebugLog(`fetchAndEnqueueSnapshotChange: missing runFriendlyId`, { source }); + return; + } + + const latestSnapshot = await this.httpClient.getRunExecutionData(this.runFriendlyId); + + if (!latestSnapshot.success) { + this.sendDebugLog(`fetchAndEnqueueSnapshotChange: failed to get latest snapshot data`, { + source, + error: latestSnapshot.error, + }); + return; + } + + const [error] = await tryCatch( + this.enqueueSnapshotChangeAndWait(latestSnapshot.data.execution) + ); + + if (error) { + this.sendDebugLog( + `fetchAndEnqueueSnapshotChange: failed to enqueue and process snapshot change`, + { + source, + error: error.message, + } + ); + return; + } + } } diff --git a/packages/cli-v3/src/entryPoints/managed/notifier.ts b/packages/cli-v3/src/entryPoints/managed/notifier.ts new file mode 100644 index 0000000000..2685f81e6c --- /dev/null +++ b/packages/cli-v3/src/entryPoints/managed/notifier.ts @@ -0,0 +1,89 @@ +import type { SupervisorSocket } from "./controller.js"; +import type { RunLogger, SendDebugLogOptions } from "./logger.js"; + +type OnNotify = (source: string) => Promise; + +type RunNotifierOptions = { + runFriendlyId: string; + supervisorSocket: SupervisorSocket; + onNotify: OnNotify; + logger: RunLogger; +}; + +export class RunNotifier { + private runFriendlyId: string; + private socket: SupervisorSocket; + private onNotify: OnNotify; + private logger: RunLogger; + + private lastNotificationAt: Date | null = null; + private notificationCount = 0; + + private lastInvalidNotificationAt: Date | null = null; + private invalidNotificationCount = 0; + + constructor(opts: RunNotifierOptions) { + this.runFriendlyId = opts.runFriendlyId; + this.socket = opts.supervisorSocket; + this.onNotify = opts.onNotify; + this.logger = opts.logger; + } + + start(): RunNotifier { + this.sendDebugLog("start"); + + this.socket.on("run:notify", async ({ version, run }) => { + // Generate a unique ID for the notification + const notificationId = Math.random().toString(36).substring(2, 15); + + // Use this to track the notification incl. any processing + const notification = { + id: notificationId, + runId: run.friendlyId, + version, + }; + + if (run.friendlyId !== this.runFriendlyId) { + this.sendDebugLog("run:notify received invalid notification", { notification }); + + this.invalidNotificationCount++; + this.lastInvalidNotificationAt = new Date(); + + return; + } + + this.sendDebugLog("run:notify received by runner", { notification }); + + this.notificationCount++; + this.lastNotificationAt = new Date(); + + await this.onNotify(`notifier:${notificationId}`); + }); + + return this; + } + + stop() { + this.sendDebugLog("stop"); + this.socket.removeAllListeners("run:notify"); + } + + get metrics() { + return { + lastNotificationAt: this.lastNotificationAt, + notificationCount: this.notificationCount, + lastInvalidNotificationAt: this.lastInvalidNotificationAt, + invalidNotificationCount: this.invalidNotificationCount, + }; + } + + private sendDebugLog(message: string, properties?: SendDebugLogOptions["properties"]) { + this.logger?.sendDebugLog({ + message: `[notifier] ${message}`, + properties: { + ...properties, + ...this.metrics, + }, + }); + } +} diff --git a/packages/cli-v3/src/entryPoints/managed/poller.ts b/packages/cli-v3/src/entryPoints/managed/poller.ts index fb4c5fb8f8..b26aa5ab21 100644 --- a/packages/cli-v3/src/entryPoints/managed/poller.ts +++ b/packages/cli-v3/src/entryPoints/managed/poller.ts @@ -1,14 +1,14 @@ -import { WorkloadHttpClient } from "@trigger.dev/core/v3/runEngineWorker"; import { RunLogger, SendDebugLogOptions } from "./logger.js"; -import { IntervalService, RunExecutionData } from "@trigger.dev/core/v3"; +import { IntervalService } from "@trigger.dev/core/v3"; + +type OnPoll = (source: string) => Promise; export type RunExecutionSnapshotPollerOptions = { runFriendlyId: string; snapshotFriendlyId: string; - httpClient: WorkloadHttpClient; logger: RunLogger; snapshotPollIntervalSeconds: number; - handleSnapshotChange: (execution: RunExecutionData) => Promise; + onPoll: OnPoll; }; export class RunExecutionSnapshotPoller { @@ -16,19 +16,20 @@ export class RunExecutionSnapshotPoller { private snapshotFriendlyId: string; private enabled: boolean; - private readonly httpClient: WorkloadHttpClient; private readonly logger: RunLogger; - private readonly handleSnapshotChange: (runData: RunExecutionData) => Promise; + private readonly onPoll: OnPoll; private readonly poller: IntervalService; + private lastPollAt: Date | null = null; + private pollCount = 0; + constructor(opts: RunExecutionSnapshotPollerOptions) { this.enabled = false; this.runFriendlyId = opts.runFriendlyId; this.snapshotFriendlyId = opts.snapshotFriendlyId; - this.httpClient = opts.httpClient; this.logger = opts.logger; - this.handleSnapshotChange = opts.handleSnapshotChange; + this.onPoll = opts.onPoll; const intervalMs = opts.snapshotPollIntervalSeconds * 1000; @@ -41,19 +42,10 @@ export class RunExecutionSnapshotPoller { this.sendDebugLog("polling for latest snapshot"); - const response = await this.httpClient.getRunExecutionData(this.runFriendlyId); - - if (!response.success) { - this.sendDebugLog("failed to get run execution data", { error: response.error }); - return; - } - - if (!this.enabled) { - this.sendDebugLog("poller disabled, skipping snapshot change handler (post)"); - return; - } + this.lastPollAt = new Date(); + this.pollCount++; - await this.handleSnapshotChange(response.data.execution); + await this.onPoll("poller"); }, intervalMs, leadingEdge: false, @@ -65,19 +57,6 @@ export class RunExecutionSnapshotPoller { }); } - private sendDebugLog(message: string, properties?: SendDebugLogOptions["properties"]) { - this.logger.sendDebugLog({ - runId: this.runFriendlyId, - message: `[poller] ${message}`, - properties: { - ...properties, - runId: this.runFriendlyId, - snapshotId: this.snapshotFriendlyId, - pollIntervalMs: this.poller.intervalMs, - }, - }); - } - resetCurrentInterval() { this.poller.resetCurrentInterval(); } @@ -121,4 +100,24 @@ export class RunExecutionSnapshotPoller { this.sendDebugLog("stopped while executing"); } } + + get metrics() { + return { + lastPollAt: this.lastPollAt, + pollCount: this.pollCount, + }; + } + + private sendDebugLog(message: string, properties?: SendDebugLogOptions["properties"]) { + this.logger.sendDebugLog({ + runId: this.runFriendlyId, + message: `[poller] ${message}`, + properties: { + ...properties, + ...this.metrics, + snapshotId: this.snapshotFriendlyId, + pollIntervalMs: this.poller.intervalMs, + }, + }); + } } From 52a57a449ec485938df20409d6b6ab3143f78e11 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 14:13:43 +0100 Subject: [PATCH 48/62] improve deprecated execution detection --- .../src/entryPoints/managed/execution.ts | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index fca29c85e5..4dda819a09 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -448,13 +448,13 @@ export class RunExecution { snapshotFriendlyId: this.snapshotManager.snapshotId, logger: this.logger, snapshotPollIntervalSeconds: this.env.TRIGGER_SNAPSHOT_POLL_INTERVAL_SECONDS, - onPoll: this.fetchAndEnqueueSnapshotChange.bind(this), + onPoll: this.fetchAndProcessSnapshotChanges.bind(this), }).start(); this.notifier = new RunNotifier({ runFriendlyId: this.runFriendlyId, supervisorSocket: this.supervisorSocket, - onNotify: this.fetchAndEnqueueSnapshotChange.bind(this), + onNotify: this.fetchAndProcessSnapshotChanges.bind(this), logger: this.logger, }).start(); @@ -1081,29 +1081,71 @@ export class RunExecution { * Fetches the latest execution data and enqueues snapshot changes. Used by both poller and notification handlers. * @param source string - where this call originated (e.g. 'poller', 'notification') */ - public async fetchAndEnqueueSnapshotChange(source: string): Promise { + public async fetchAndProcessSnapshotChanges(source: string): Promise { if (!this.runFriendlyId) { - this.sendDebugLog(`fetchAndEnqueueSnapshotChange: missing runFriendlyId`, { source }); + this.sendDebugLog(`fetchAndProcessSnapshotChanges: missing runFriendlyId`, { source }); return; } - const latestSnapshot = await this.httpClient.getRunExecutionData(this.runFriendlyId); + // Use the last processed snapshot as the since parameter + const sinceSnapshotId = this.currentSnapshotFriendlyId; - if (!latestSnapshot.success) { - this.sendDebugLog(`fetchAndEnqueueSnapshotChange: failed to get latest snapshot data`, { + if (!sinceSnapshotId) { + this.sendDebugLog(`fetchAndProcessSnapshotChanges: missing sinceSnapshotId`, { source }); + return; + } + + const response = await this.httpClient.getSnapshotsSince(this.runFriendlyId, sinceSnapshotId); + + if (!response.success) { + this.sendDebugLog(`fetchAndProcessSnapshotChanges: failed to get snapshots since`, { source, - error: latestSnapshot.error, + error: response.error, }); return; } - const [error] = await tryCatch( - this.enqueueSnapshotChangeAndWait(latestSnapshot.data.execution) + const { executions } = response.data; + + if (!executions.length) { + this.sendDebugLog(`fetchAndProcessSnapshotChanges: no new snapshots`, { source }); + return; + } + + // Only act on the last snapshot + const lastSnapshot = executions[executions.length - 1]; + + if (!lastSnapshot) { + this.sendDebugLog(`fetchAndProcessSnapshotChanges: no last snapshot`, { source }); + return; + } + + const previousSnapshots = executions.slice(0, -1); + + // If any previous snapshot is QUEUED or SUSPENDED, deprecate this worker + const deprecatedStatus: TaskRunExecutionStatus[] = ["QUEUED", "SUSPENDED"]; + const foundDeprecated = previousSnapshots.find((snap) => + deprecatedStatus.includes(snap.snapshot.executionStatus) ); + if (foundDeprecated) { + this.sendDebugLog( + `fetchAndProcessSnapshotChanges: found deprecation marker in previous snapshots, exiting`, + { + source, + status: foundDeprecated.snapshot.executionStatus, + snapshotId: foundDeprecated.snapshot.friendlyId, + } + ); + await this.exitTaskRunProcessWithoutFailingRun({ flush: false }); + return; + } + + const [error] = await tryCatch(this.enqueueSnapshotChangeAndWait(lastSnapshot)); + if (error) { this.sendDebugLog( - `fetchAndEnqueueSnapshotChange: failed to enqueue and process snapshot change`, + `fetchAndProcessSnapshotChanges: failed to enqueue and process snapshot change`, { source, error: error.message, From 16b344d67c9eb2ece1efc1e0cdc8e932bd61b8cc Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 14:44:46 +0100 Subject: [PATCH 49/62] update supervisor and schema --- apps/supervisor/src/workloadServer/index.ts | 26 +++++++++++++++++++ ...nFriendlyId.snapshots.since.$snapshotId.ts | 6 ++--- .../src/entryPoints/managed/execution.ts | 8 +++--- .../src/v3/runEngineWorker/supervisor/http.ts | 15 +++++++++++ .../v3/runEngineWorker/supervisor/schemas.ts | 2 +- 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/apps/supervisor/src/workloadServer/index.ts b/apps/supervisor/src/workloadServer/index.ts index 2dcf329736..cc41e2bfbf 100644 --- a/apps/supervisor/src/workloadServer/index.ts +++ b/apps/supervisor/src/workloadServer/index.ts @@ -17,6 +17,7 @@ import { WorkloadRunAttemptStartRequestBody, type WorkloadRunAttemptStartResponseBody, type WorkloadRunLatestSnapshotResponseBody, + WorkloadRunSnapshotsSinceResponseBody, type WorkloadServerToClientEvents, type WorkloadSuspendRunResponseBody, } from "@trigger.dev/core/v3/workers"; @@ -341,6 +342,31 @@ export class WorkloadServer extends EventEmitter { } satisfies WorkloadRunLatestSnapshotResponseBody); }, }) + .route( + "/api/v1/workload-actions/runs/:runFriendlyId/snapshots/since/:snapshotFriendlyId", + "GET", + { + paramsSchema: WorkloadActionParams, + handler: async ({ req, reply, params }) => { + const sinceSnapshotResponse = await this.workerClient.getSnapshotsSince( + params.runFriendlyId, + params.snapshotFriendlyId, + this.runnerIdFromRequest(req) + ); + + if (!sinceSnapshotResponse.success) { + console.error("Failed to get snapshots since", { + runId: params.runFriendlyId, + error: sinceSnapshotResponse.error, + }); + reply.empty(500); + return; + } + + reply.json(sinceSnapshotResponse.data satisfies WorkloadRunSnapshotsSinceResponseBody); + }, + } + ) .route("/api/v1/workload-actions/runs/:runFriendlyId/logs/debug", "POST", { paramsSchema: WorkloadActionParams.pick({ runFriendlyId: true }), bodySchema: WorkloadDebugLogRequestBody, diff --git a/apps/webapp/app/routes/engine.v1.worker-actions.runs.$runFriendlyId.snapshots.since.$snapshotId.ts b/apps/webapp/app/routes/engine.v1.worker-actions.runs.$runFriendlyId.snapshots.since.$snapshotId.ts index 1531205c24..a79de5869a 100644 --- a/apps/webapp/app/routes/engine.v1.worker-actions.runs.$runFriendlyId.snapshots.since.$snapshotId.ts +++ b/apps/webapp/app/routes/engine.v1.worker-actions.runs.$runFriendlyId.snapshots.since.$snapshotId.ts @@ -16,15 +16,15 @@ export const loader = createLoaderWorkerApiRoute( }): Promise> => { const { runFriendlyId, snapshotId } = params; - const executions = await authenticatedWorker.getSnapshotsSince({ + const snapshots = await authenticatedWorker.getSnapshotsSince({ runFriendlyId, snapshotId, }); - if (!executions) { + if (!snapshots) { throw new Error("Failed to retrieve snapshots since given snapshot"); } - return json({ executions }); + return json({ snapshots }); } ); diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 4dda819a09..1c0b636fa7 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -1105,22 +1105,22 @@ export class RunExecution { return; } - const { executions } = response.data; + const { snapshots } = response.data; - if (!executions.length) { + if (!snapshots.length) { this.sendDebugLog(`fetchAndProcessSnapshotChanges: no new snapshots`, { source }); return; } // Only act on the last snapshot - const lastSnapshot = executions[executions.length - 1]; + const lastSnapshot = snapshots[snapshots.length - 1]; if (!lastSnapshot) { this.sendDebugLog(`fetchAndProcessSnapshotChanges: no last snapshot`, { source }); return; } - const previousSnapshots = executions.slice(0, -1); + const previousSnapshots = snapshots.slice(0, -1); // If any previous snapshot is QUEUED or SUSPENDED, deprecate this worker const deprecatedStatus: TaskRunExecutionStatus[] = ["QUEUED", "SUSPENDED"]; diff --git a/packages/core/src/v3/runEngineWorker/supervisor/http.ts b/packages/core/src/v3/runEngineWorker/supervisor/http.ts index 4f899e4f22..43305b456a 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/http.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/http.ts @@ -17,6 +17,7 @@ import { WorkerApiDebugLogBody, WorkerApiSuspendRunRequestBody, WorkerApiSuspendRunResponseBody, + WorkerApiRunSnapshotsSinceResponseBody, } from "./schemas.js"; import { SupervisorClientCommonOptions } from "./types.js"; import { getDefaultWorkerHeaders } from "./util.js"; @@ -185,6 +186,20 @@ export class SupervisorHttpClient { ); } + async getSnapshotsSince(runId: string, snapshotId: string, runnerId?: string) { + return wrapZodFetch( + WorkerApiRunSnapshotsSinceResponseBody, + `${this.apiUrl}/engine/v1/worker-actions/runs/${runId}/snapshots/since/${snapshotId}`, + { + method: "GET", + headers: { + ...this.defaultHeaders, + ...this.runnerIdHeader(runnerId), + }, + } + ); + } + async sendDebugLog(runId: string, body: WorkerApiDebugLogBody, runnerId?: string): Promise { try { const res = await wrapZodFetch( diff --git a/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts b/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts index deeaadf47d..a49fd53a09 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts @@ -164,7 +164,7 @@ export type WorkerApiSuspendCompletionResponseBody = z.infer< >; export const WorkerApiRunSnapshotsSinceResponseBody = z.object({ - executions: z.array(RunExecutionData), + snapshots: z.array(RunExecutionData), }); export type WorkerApiRunSnapshotsSinceResponseBody = z.infer< typeof WorkerApiRunSnapshotsSinceResponseBody From b09178adb8c2605d0af4d65a09b0984f86815b17 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 15:10:01 +0100 Subject: [PATCH 50/62] properly log http server errors --- packages/core/src/v3/serverOnly/httpServer.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/core/src/v3/serverOnly/httpServer.ts b/packages/core/src/v3/serverOnly/httpServer.ts index 3aa327a8bd..bdbfcb7589 100644 --- a/packages/core/src/v3/serverOnly/httpServer.ts +++ b/packages/core/src/v3/serverOnly/httpServer.ts @@ -201,7 +201,7 @@ export class HttpServer { ); if (error) { - logger.error("Route handler error", { error }); + logger.error("Route handler error", { error: this.formatError(error) }); return reply.empty(500); } @@ -210,7 +210,7 @@ export class HttpServer { return; } } catch (error) { - logger.error("Failed to handle request", { error }); + logger.error("Failed to handle request", { error: this.formatError(error) }); return reply.empty(500); } finally { this.collectMetrics(req, res, startTime); @@ -364,4 +364,16 @@ export class HttpServer { return null; } + + private formatError(error: unknown): string | Record { + if (error instanceof Error) { + return { + name: error.name, + message: error.message, + stack: error.stack, + }; + } + + return String(error); + } } From 59d9784a1bb3e50d8b2cbfbab80030d3ad39ed50 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 15:19:26 +0100 Subject: [PATCH 51/62] detect restore after failed snapshot fetch --- packages/cli-v3/src/entryPoints/managed/execution.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 1c0b636fa7..b1e02b2661 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -252,8 +252,6 @@ export class RunExecution { this.snapshotPoller?.updateSnapshotId(snapshot.friendlyId); this.snapshotPoller?.resetCurrentInterval(); - await this.processEnvOverrides("snapshot change"); - switch (snapshot.executionStatus) { case "PENDING_CANCEL": { this.sendDebugLog("run was cancelled", snapshotMetadata); @@ -1102,6 +1100,9 @@ export class RunExecution { source, error: response.error, }); + + await this.processEnvOverrides("snapshots since error"); + return; } From e26dcb207960d64bef98cea7e6ac951bce85fcbe Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 16:39:23 +0100 Subject: [PATCH 52/62] run and snapshot id can be overridden --- .../cli-v3/src/entryPoints/managed/env.ts | 24 ++++++++++++------- .../src/entryPoints/managed/overrides.ts | 2 ++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/env.ts b/packages/cli-v3/src/entryPoints/managed/env.ts index d3e381fa26..87b5483354 100644 --- a/packages/cli-v3/src/entryPoints/managed/env.ts +++ b/packages/cli-v3/src/entryPoints/managed/env.ts @@ -22,8 +22,6 @@ const Env = z.object({ // Set at runtime TRIGGER_WORKLOAD_CONTROLLER_ID: z.string().default(`controller_${randomUUID()}`), TRIGGER_ENV_ID: z.string(), - TRIGGER_RUN_ID: z.string().optional(), // This is only useful for cold starts - TRIGGER_SNAPSHOT_ID: z.string().optional(), // This is only useful for cold starts OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url(), TRIGGER_WARM_START_URL: z.string().optional(), TRIGGER_WARM_START_CONNECTION_TIMEOUT_MS: z.coerce.number().default(30_000), @@ -38,6 +36,8 @@ const Env = z.object({ TRIGGER_DEQUEUED_AT_MS: DateEnv, // May be overridden + TRIGGER_RUN_ID: z.string().optional(), // This is set for cold starts and restores + TRIGGER_SNAPSHOT_ID: z.string().optional(), // This is set for cold starts and restores TRIGGER_SUPERVISOR_API_PROTOCOL: z.enum(["http", "https"]), TRIGGER_SUPERVISOR_API_DOMAIN: z.string(), TRIGGER_SUPERVISOR_API_PORT: z.coerce.number(), @@ -94,12 +94,6 @@ export class RunnerEnv { get TRIGGER_ENV_ID() { return this.env.TRIGGER_ENV_ID; } - get TRIGGER_RUN_ID() { - return this.env.TRIGGER_RUN_ID; - } - get TRIGGER_SNAPSHOT_ID() { - return this.env.TRIGGER_SNAPSHOT_ID; - } get TRIGGER_WARM_START_URL() { return this.env.TRIGGER_WARM_START_URL; } @@ -126,6 +120,12 @@ export class RunnerEnv { } // Overridable values + get TRIGGER_RUN_ID() { + return this.env.TRIGGER_RUN_ID; + } + get TRIGGER_SNAPSHOT_ID() { + return this.env.TRIGGER_SNAPSHOT_ID; + } get TRIGGER_SUCCESS_EXIT_CODE() { return this.env.TRIGGER_SUCCESS_EXIT_CODE; } @@ -163,6 +163,14 @@ export class RunnerEnv { /** Overrides existing env vars with new values */ override(overrides: Metadata) { + if (overrides.TRIGGER_RUN_ID) { + this.env.TRIGGER_RUN_ID = overrides.TRIGGER_RUN_ID; + } + + if (overrides.TRIGGER_SNAPSHOT_ID) { + this.env.TRIGGER_SNAPSHOT_ID = overrides.TRIGGER_SNAPSHOT_ID; + } + if (overrides.TRIGGER_SUCCESS_EXIT_CODE) { this.env.TRIGGER_SUCCESS_EXIT_CODE = overrides.TRIGGER_SUCCESS_EXIT_CODE; } diff --git a/packages/cli-v3/src/entryPoints/managed/overrides.ts b/packages/cli-v3/src/entryPoints/managed/overrides.ts index 872b5ad0b3..862e6c6fb0 100644 --- a/packages/cli-v3/src/entryPoints/managed/overrides.ts +++ b/packages/cli-v3/src/entryPoints/managed/overrides.ts @@ -1,4 +1,6 @@ export type Metadata = { + TRIGGER_RUN_ID: string | undefined; + TRIGGER_SNAPSHOT_ID: string | undefined; TRIGGER_SUPERVISOR_API_PROTOCOL: string | undefined; TRIGGER_SUPERVISOR_API_DOMAIN: string | undefined; TRIGGER_SUPERVISOR_API_PORT: number | undefined; From 7c1a816981f0dd8cba2cf7efed1fa97ec251667c Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 17:41:23 +0100 Subject: [PATCH 53/62] fix restore detection --- .../src/entryPoints/managed/execution.ts | 81 +++++++++++++------ .../src/entryPoints/managed/overrides.ts | 12 ++- 2 files changed, 66 insertions(+), 27 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index b1e02b2661..f325122790 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -17,7 +17,7 @@ import { WorkloadHttpClient } from "@trigger.dev/core/v3/workers"; import { setTimeout as sleep } from "timers/promises"; import { RunExecutionSnapshotPoller } from "./poller.js"; import { assertExhaustive, tryCatch } from "@trigger.dev/core/utils"; -import { MetadataClient } from "./overrides.js"; +import { Metadata, MetadataClient } from "./overrides.js"; import { randomBytes } from "node:crypto"; import { SnapshotManager, SnapshotState } from "./snapshot.js"; import type { SupervisorSocket } from "./controller.js"; @@ -76,6 +76,7 @@ export class RunExecution { private supervisorSocket: SupervisorSocket; private notifier?: RunNotifier; + private metadataClient?: MetadataClient; constructor(opts: RunExecutionOptions) { this.id = randomBytes(4).toString("hex"); @@ -87,6 +88,10 @@ export class RunExecution { this.restoreCount = 0; this.executionAbortController = new AbortController(); + + if (this.env.TRIGGER_METADATA_URL) { + this.metadataClient = new MetadataClient(this.env.TRIGGER_METADATA_URL); + } } /** @@ -820,30 +825,42 @@ export class RunExecution { /** * Processes env overrides from the metadata service. Generally called when we're resuming from a suspended state. */ - public async processEnvOverrides(reason?: string) { - if (!this.env.TRIGGER_METADATA_URL) { - this.sendDebugLog("no metadata url, skipping env overrides", { reason }); - return; + public async processEnvOverrides( + reason?: string + ): Promise<{ executionWasRestored: boolean; overrides: Metadata } | null> { + if (!this.metadataClient) { + return null; } - const metadataClient = new MetadataClient(this.env.TRIGGER_METADATA_URL); - const overrides = await metadataClient.getEnvOverrides(); + const [error, overrides] = await this.metadataClient.getEnvOverrides(); - if (!overrides) { - this.sendDebugLog("no env overrides, skipping", { reason }); - return; + if (error) { + this.sendDebugLog("[override] failed to fetch", { error: error.message }); + return null; + } + + if (overrides.TRIGGER_RUN_ID && overrides.TRIGGER_RUN_ID !== this.runFriendlyId) { + this.sendDebugLog("[override] run ID mismatch, ignoring overrides", { + currentRunId: this.runFriendlyId, + overrideRunId: overrides.TRIGGER_RUN_ID, + }); + return null; } - this.sendDebugLog(`processing env overrides: ${reason}`, { + this.sendDebugLog(`[override] processing: ${reason}`, { overrides, currentEnv: this.env.raw, }); + let executionWasRestored = false; + if (this.env.TRIGGER_RUNNER_ID !== overrides.TRIGGER_RUNNER_ID) { - this.sendDebugLog("runner ID changed -> run was restored from a checkpoint", { + this.sendDebugLog("[override] runner ID changed -> execution was restored", { currentRunnerId: this.env.TRIGGER_RUNNER_ID, newRunnerId: overrides.TRIGGER_RUNNER_ID, }); + + executionWasRestored = true; } // Override the env with the new values @@ -863,6 +880,11 @@ export class RunExecution { if (overrides.TRIGGER_RUNNER_ID) { this.httpClient.updateRunnerId(this.env.TRIGGER_RUNNER_ID); } + + return { + executionWasRestored, + overrides, + }; } private async onHeartbeat() { @@ -1125,21 +1147,34 @@ export class RunExecution { // If any previous snapshot is QUEUED or SUSPENDED, deprecate this worker const deprecatedStatus: TaskRunExecutionStatus[] = ["QUEUED", "SUSPENDED"]; - const foundDeprecated = previousSnapshots.find((snap) => + const deprecatedSnapshots = previousSnapshots.filter((snap) => deprecatedStatus.includes(snap.snapshot.executionStatus) ); - if (foundDeprecated) { - this.sendDebugLog( - `fetchAndProcessSnapshotChanges: found deprecation marker in previous snapshots, exiting`, - { - source, - status: foundDeprecated.snapshot.executionStatus, - snapshotId: foundDeprecated.snapshot.friendlyId, - } + if (deprecatedSnapshots.length) { + const result = await this.processEnvOverrides( + "found deprecation marker in previous snapshots" ); - await this.exitTaskRunProcessWithoutFailingRun({ flush: false }); - return; + + if (!result) { + return; + } + + const { executionWasRestored } = result; + + if (executionWasRestored) { + // It's normal for a restored run to have deprecation markers, e.g. it will have been SUSPENDED + } else { + this.sendDebugLog( + `fetchAndProcessSnapshotChanges: found deprecation marker in previous snapshots, exiting`, + { + source, + deprecatedSnapshots: deprecatedSnapshots.map((s) => s.snapshot), + } + ); + await this.exitTaskRunProcessWithoutFailingRun({ flush: false }); + return; + } } const [error] = await tryCatch(this.enqueueSnapshotChangeAndWait(lastSnapshot)); diff --git a/packages/cli-v3/src/entryPoints/managed/overrides.ts b/packages/cli-v3/src/entryPoints/managed/overrides.ts index 862e6c6fb0..85c5607105 100644 --- a/packages/cli-v3/src/entryPoints/managed/overrides.ts +++ b/packages/cli-v3/src/entryPoints/managed/overrides.ts @@ -19,13 +19,17 @@ export class MetadataClient { this.url = new URL(url); } - async getEnvOverrides(): Promise { + async getEnvOverrides(): Promise<[error: Error, data: null] | [error: null, data: Metadata]> { try { const response = await fetch(new URL("/env", this.url)); - return response.json(); + + if (!response.ok) { + return [new Error(`Status ${response.status} ${response.statusText}`), null]; + } + + return [null, await response.json()]; } catch (error) { - console.error("Failed to fetch metadata", { error }); - return null; + return [error instanceof Error ? error : new Error(String(error)), null]; } } } From 6b16df09ef3ab0a10c86a8c48a014de6331ff534 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 19:59:24 +0100 Subject: [PATCH 54/62] fix deprecation checks, move into snapshot manager --- .../src/entryPoints/managed/execution.ts | 80 +++---- .../src/entryPoints/managed/snapshot.test.ts | 204 ++++++++++++------ .../src/entryPoints/managed/snapshot.ts | 111 ++++++++-- 3 files changed, 266 insertions(+), 129 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index f325122790..34a06a0da6 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -216,7 +216,7 @@ export class RunExecution { * * This is the main entry point for snapshot changes, but processing is deferred to the snapshot manager. */ - public async enqueueSnapshotChangeAndWait(runData: RunExecutionData): Promise { + private async enqueueSnapshotChangesAndWait(snapshots: RunExecutionData[]): Promise { if (this.isShuttingDown) { this.sendDebugLog("enqueueSnapshotChangeAndWait: shutting down, skipping"); return; @@ -227,10 +227,13 @@ export class RunExecution { return; } - await this.snapshotManager.handleSnapshotChange(runData); + await this.snapshotManager.handleSnapshotChanges(snapshots); } - private async processSnapshotChange(runData: RunExecutionData): Promise { + private async processSnapshotChange( + runData: RunExecutionData, + deprecated: boolean + ): Promise { const { run, snapshot, completedWaitpoints } = runData; const snapshotMetadata = { @@ -257,6 +260,13 @@ export class RunExecution { this.snapshotPoller?.updateSnapshotId(snapshot.friendlyId); this.snapshotPoller?.resetCurrentInterval(); + if (deprecated) { + this.sendDebugLog("run execution is deprecated", { incomingSnapshot: snapshot }); + + await this.exitTaskRunProcessWithoutFailingRun({ flush: false }); + return; + } + switch (snapshot.executionStatus) { case "PENDING_CANCEL": { this.sendDebugLog("run was cancelled", snapshotMetadata); @@ -276,14 +286,12 @@ export class RunExecution { case "QUEUED": { this.sendDebugLog("run was re-queued", snapshotMetadata); - // Pretend we've just suspended the run. This will kill the process without failing the run. await this.exitTaskRunProcessWithoutFailingRun({ flush: true }); return; } case "FINISHED": { this.sendDebugLog("run is finished", snapshotMetadata); - // Pretend we've just suspended the run. This will kill the process without failing the run. await this.exitTaskRunProcessWithoutFailingRun({ flush: true }); return; } @@ -434,10 +442,12 @@ export class RunExecution { // Create snapshot manager this.snapshotManager = new SnapshotManager({ runFriendlyId: runOpts.runFriendlyId, + runnerId: this.env.TRIGGER_RUNNER_ID, initialSnapshotId: runOpts.snapshotFriendlyId, // We're just guessing here, but "PENDING_EXECUTING" is probably fine initialStatus: "PENDING_EXECUTING", logger: this.logger, + metadataClient: this.metadataClient, onSnapshotChange: this.processSnapshotChange.bind(this), onSuspendable: this.handleSuspendable.bind(this), }); @@ -835,14 +845,18 @@ export class RunExecution { const [error, overrides] = await this.metadataClient.getEnvOverrides(); if (error) { - this.sendDebugLog("[override] failed to fetch", { error: error.message }); + this.sendDebugLog("[override] failed to fetch", { + reason, + error: error.message, + }); return null; } if (overrides.TRIGGER_RUN_ID && overrides.TRIGGER_RUN_ID !== this.runFriendlyId) { this.sendDebugLog("[override] run ID mismatch, ignoring overrides", { + reason, currentRunId: this.runFriendlyId, - overrideRunId: overrides.TRIGGER_RUN_ID, + incomingRunId: overrides.TRIGGER_RUN_ID, }); return null; } @@ -855,11 +869,14 @@ export class RunExecution { let executionWasRestored = false; if (this.env.TRIGGER_RUNNER_ID !== overrides.TRIGGER_RUNNER_ID) { - this.sendDebugLog("[override] runner ID changed -> execution was restored", { + this.sendDebugLog("[override] runner ID mismatch, execution was restored", { + reason, currentRunnerId: this.env.TRIGGER_RUNNER_ID, - newRunnerId: overrides.TRIGGER_RUNNER_ID, + incomingRunnerId: overrides.TRIGGER_RUNNER_ID, }); + // we should keep a list of restored snapshots + executionWasRestored = true; } @@ -1124,7 +1141,6 @@ export class RunExecution { }); await this.processEnvOverrides("snapshots since error"); - return; } @@ -1135,49 +1151,7 @@ export class RunExecution { return; } - // Only act on the last snapshot - const lastSnapshot = snapshots[snapshots.length - 1]; - - if (!lastSnapshot) { - this.sendDebugLog(`fetchAndProcessSnapshotChanges: no last snapshot`, { source }); - return; - } - - const previousSnapshots = snapshots.slice(0, -1); - - // If any previous snapshot is QUEUED or SUSPENDED, deprecate this worker - const deprecatedStatus: TaskRunExecutionStatus[] = ["QUEUED", "SUSPENDED"]; - const deprecatedSnapshots = previousSnapshots.filter((snap) => - deprecatedStatus.includes(snap.snapshot.executionStatus) - ); - - if (deprecatedSnapshots.length) { - const result = await this.processEnvOverrides( - "found deprecation marker in previous snapshots" - ); - - if (!result) { - return; - } - - const { executionWasRestored } = result; - - if (executionWasRestored) { - // It's normal for a restored run to have deprecation markers, e.g. it will have been SUSPENDED - } else { - this.sendDebugLog( - `fetchAndProcessSnapshotChanges: found deprecation marker in previous snapshots, exiting`, - { - source, - deprecatedSnapshots: deprecatedSnapshots.map((s) => s.snapshot), - } - ); - await this.exitTaskRunProcessWithoutFailingRun({ flush: false }); - return; - } - } - - const [error] = await tryCatch(this.enqueueSnapshotChangeAndWait(lastSnapshot)); + const [error] = await tryCatch(this.enqueueSnapshotChangesAndWait(snapshots)); if (error) { this.sendDebugLog( diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts index 1efd0de6ca..bf3b4668d8 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts @@ -14,6 +14,7 @@ describe("SnapshotManager", () => { beforeEach(() => { vi.clearAllMocks(); manager = new SnapshotManager({ + runnerId: "test-runner-1", runFriendlyId: "test-run-1", initialSnapshotId: "snapshot-1", initialStatus: "PENDING_EXECUTING", @@ -48,12 +49,12 @@ describe("SnapshotManager", () => { expect(mockSuspendableHandler).not.toHaveBeenCalled(); // When status changes to EXECUTING_WITH_WAITPOINTS, suspendable handler should be called - await manager.handleSnapshotChange( + await manager.handleSnapshotChanges([ createRunExecutionData({ snapshotId: "snapshot-2", executionStatus: "EXECUTING_WITH_WAITPOINTS", - }) - ); + }), + ]); expect(mockSuspendableHandler).toHaveBeenCalledWith({ id: "snapshot-2", @@ -96,12 +97,12 @@ describe("SnapshotManager", () => { vi.clearAllMocks(); // Transitioning to QUEUED_EXECUTING should call the handler again - await manager.handleSnapshotChange( + await manager.handleSnapshotChanges([ createRunExecutionData({ snapshotId: "snapshot-3", executionStatus: "QUEUED_EXECUTING", - }) - ); + }), + ]); expect(mockSuspendableHandler).toHaveBeenCalledWith({ id: "snapshot-3", status: "QUEUED_EXECUTING", @@ -113,6 +114,7 @@ describe("SnapshotManager", () => { // Create a manager with handlers that track execution order const manager = new SnapshotManager({ + runnerId: "test-runner-1", runFriendlyId: "test-run-1", initialSnapshotId: "snapshot-1", initialStatus: "PENDING_EXECUTING", @@ -129,14 +131,14 @@ describe("SnapshotManager", () => { const promises = [ manager.setSuspendable(false), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-2" })]), manager.setSuspendable(true), - manager.handleSnapshotChange( + manager.handleSnapshotChanges([ createRunExecutionData({ snapshotId: "snapshot-3", executionStatus: "EXECUTING_WITH_WAITPOINTS", - }) - ), + }), + ]), ]; await Promise.all(promises); @@ -155,6 +157,7 @@ describe("SnapshotManager", () => { const executionOrder: string[] = []; const manager = new SnapshotManager({ + runnerId: "test-runner-1", runFriendlyId: "test-run-1", initialSnapshotId: "snapshot-1", initialStatus: "PENDING_EXECUTING", @@ -167,9 +170,9 @@ describe("SnapshotManager", () => { // Queue snapshots in reverse order const promises = [ - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-3" })), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-1" })), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-3" })]), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-2" })]), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-1" })]), ]; await Promise.all(promises); @@ -182,6 +185,7 @@ describe("SnapshotManager", () => { const executionOrder: string[] = []; const manager = new SnapshotManager({ + runnerId: "test-runner-1", runFriendlyId: "test-run-1", initialSnapshotId: "snapshot-1", initialStatus: "PENDING_EXECUTING", @@ -194,9 +198,9 @@ describe("SnapshotManager", () => { // Queue snapshots in reverse order const promises = [ - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-2" })]), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-2" })]), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-2" })]), ]; await Promise.all(promises); @@ -210,6 +214,7 @@ describe("SnapshotManager", () => { let currentlyExecuting = false; const manager = new SnapshotManager({ + runnerId: "test-runner-1", runFriendlyId: "test-run-1", initialSnapshotId: "snapshot-1", initialStatus: "PENDING_EXECUTING", @@ -241,23 +246,23 @@ describe("SnapshotManager", () => { // Create a mix of rapid changes const promises = [ manager.setSuspendable(true), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-2" })]), manager.setSuspendable(false), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-3" })), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-3" })]), manager.setSuspendable(true), manager.setSuspendable(true), manager.setSuspendable(false), manager.setSuspendable(false), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-4" })), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-4" })]), manager.setSuspendable(false), manager.setSuspendable(true), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-1" })), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-1" })]), manager.setSuspendable(false), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-3" })), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-3" })]), manager.setSuspendable(true), manager.setSuspendable(true), manager.setSuspendable(false), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-2" })]), manager.setSuspendable(false), ]; @@ -277,6 +282,7 @@ describe("SnapshotManager", () => { let shouldThrowSuspendableError = false; const manager = new SnapshotManager({ + runnerId: "test-runner-1", runFriendlyId: "test-run-1", initialSnapshotId: "snapshot-1", initialStatus: "PENDING_EXECUTING", @@ -299,9 +305,9 @@ describe("SnapshotManager", () => { // Queue up some changes const initialPromises = [ - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-2" })), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-2" })]), manager.setSuspendable(true), - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-3" })), + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-3" })]), ]; expect(manager.queueLength).not.toBe(0); @@ -327,17 +333,17 @@ describe("SnapshotManager", () => { // Now test error handling shouldThrowSnapshotError = true; await expect( - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "snapshot-4" })) + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "snapshot-4" })]) ).rejects.toThrow("Snapshot handler error"); // Queue should continue processing after error shouldThrowSnapshotError = false; - await manager.handleSnapshotChange( + await manager.handleSnapshotChanges([ createRunExecutionData({ snapshotId: "snapshot-5", executionStatus: "EXECUTING_WITH_WAITPOINTS", - }) - ); + }), + ]); expect(executionOrder).toEqual(["snapshot:snapshot-2", "snapshot:snapshot-5"]); // Test suspendable error @@ -365,6 +371,7 @@ describe("SnapshotManager", () => { let handlerExecutionCount = 0; const manager = new SnapshotManager({ + runnerId: "test-runner-1", runFriendlyId: "test-run-1", initialSnapshotId: "snapshot-1", initialStatus: "PENDING_EXECUTING", @@ -402,7 +409,7 @@ describe("SnapshotManager", () => { }); // Test empty snapshot IDs - await manager.handleSnapshotChange(createRunExecutionData({ snapshotId: "" })); + await manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: "" })]); expect(executionOrder).toEqual([]); // Create a very long queue of mixed changes @@ -412,7 +419,7 @@ describe("SnapshotManager", () => { for (let i = 1; i <= 50; i++) { if (i % 2 === 0) { promises.push( - manager.handleSnapshotChange(createRunExecutionData({ snapshotId: `snapshot-${i}` })) + manager.handleSnapshotChanges([createRunExecutionData({ snapshotId: `snapshot-${i}` })]) ); } else { promises.push(manager.setSuspendable(i % 4 === 1)); @@ -429,9 +436,9 @@ describe("SnapshotManager", () => { for (const id of snapshotIds) { for (let i = 0; i < 5; i++) { promises.push( - manager.handleSnapshotChange( - createRunExecutionData({ snapshotId: `snapshot-${id}-${i}` }) - ) + manager.handleSnapshotChanges([ + createRunExecutionData({ snapshotId: `snapshot-${id}-${i}` }), + ]) ); } } @@ -475,6 +482,7 @@ describe("SnapshotManager", () => { let processingCount = 0; const manager = new SnapshotManager({ + runnerId: "test-runner-1", runFriendlyId: "test-run-1", initialSnapshotId: "snapshot-1", initialStatus: "PENDING_EXECUTING", @@ -495,12 +503,12 @@ describe("SnapshotManager", () => { // Test parallel queue processing prevention const parallelPromises = Array.from({ length: 5 }, (_, i) => - manager.handleSnapshotChange( + manager.handleSnapshotChanges([ createRunExecutionData({ snapshotId: `parallel-${i}`, executionStatus: "EXECUTING", - }) - ) + }), + ]) ); // Add some suspendable changes in the middle @@ -510,12 +518,12 @@ describe("SnapshotManager", () => { // Add more snapshot changes parallelPromises.push( ...Array.from({ length: 5 }, (_, i) => - manager.handleSnapshotChange( + manager.handleSnapshotChanges([ createRunExecutionData({ snapshotId: `parallel-${i + 5}`, executionStatus: "EXECUTING", - }) - ) + }), + ]) ) ); @@ -526,42 +534,42 @@ describe("SnapshotManager", () => { // Test edge case: snapshot ID comparison with special characters const specialCharPromises = [ - manager.handleSnapshotChange( + manager.handleSnapshotChanges([ createRunExecutionData({ snapshotId: "snapshot-1!", executionStatus: "EXECUTING", - }) - ), - manager.handleSnapshotChange( + }), + ]), + manager.handleSnapshotChanges([ createRunExecutionData({ snapshotId: "snapshot-1@", executionStatus: "EXECUTING", - }) - ), - manager.handleSnapshotChange( + }), + ]), + manager.handleSnapshotChanges([ createRunExecutionData({ snapshotId: "snapshot-1#", executionStatus: "EXECUTING", - }) - ), + }), + ]), ]; await Promise.all(specialCharPromises); // Test edge case: very long snapshot IDs const longIdPromises = [ - manager.handleSnapshotChange( + manager.handleSnapshotChanges([ createRunExecutionData({ snapshotId: "a".repeat(1000), executionStatus: "EXECUTING", - }) - ), - manager.handleSnapshotChange( + }), + ]), + manager.handleSnapshotChanges([ createRunExecutionData({ snapshotId: "b".repeat(1000), executionStatus: "EXECUTING", - }) - ), + }), + ]), ]; await Promise.all(longIdPromises); @@ -569,6 +577,7 @@ describe("SnapshotManager", () => { // Test edge case: rapid queue changes during processing let isProcessing = false; const rapidChangeManager = new SnapshotManager({ + runnerId: "test-runner-1", runFriendlyId: "test-run-2", initialSnapshotId: "snapshot-1", initialStatus: "PENDING_EXECUTING", @@ -586,31 +595,31 @@ describe("SnapshotManager", () => { }); // Start processing a snapshot - const initialPromise = rapidChangeManager.handleSnapshotChange( + const initialPromise = rapidChangeManager.handleSnapshotChanges([ createRunExecutionData({ runId: "test-run-2", snapshotId: "snapshot-2", executionStatus: "EXECUTING", - }) - ); + }), + ]); // Queue more changes while the first one is processing await setTimeout(10); const queuePromises = [ - rapidChangeManager.handleSnapshotChange( + rapidChangeManager.handleSnapshotChanges([ createRunExecutionData({ runId: "test-run-2", snapshotId: "snapshot-3", executionStatus: "EXECUTING", - }) - ), - rapidChangeManager.handleSnapshotChange( + }), + ]), + rapidChangeManager.handleSnapshotChanges([ createRunExecutionData({ runId: "test-run-2", snapshotId: "snapshot-4", executionStatus: "EXECUTING", - }) - ), + }), + ]), ]; await Promise.all([initialPromise, ...queuePromises]); @@ -619,6 +628,75 @@ describe("SnapshotManager", () => { const rapidChanges = executionOrder.filter((entry) => entry.startsWith("rapid:")); expect(rapidChanges).toEqual(["rapid:snapshot-2", "rapid:snapshot-3", "rapid:snapshot-4"]); }); + + it("should detect restore and not deprecate restored runner", async () => { + // Mock MetadataClient + let runnerId = "test-runner-1"; + const mockMetadataClient = { + getEnvOverrides: vi.fn().mockImplementation(() => { + return Promise.resolve([null, { TRIGGER_RUNNER_ID: runnerId }]); + }), + }; + + const onSnapshotChange = vi.fn(); + const manager = new SnapshotManager({ + runnerId: "test-runner-1", + runFriendlyId: "test-run-1", + initialSnapshotId: "snapshot-1", + initialStatus: "PENDING_EXECUTING", + logger: mockLogger, + metadataClient: mockMetadataClient as any, + onSnapshotChange, + onSuspendable: async () => {}, + }); + + // Simulate some basic snapshot changes + await manager.handleSnapshotChanges([ + createRunExecutionData({ snapshotId: "snapshot-2", executionStatus: "EXECUTING" }), + createRunExecutionData({ + snapshotId: "snapshot-3", + executionStatus: "EXECUTING_WITH_WAITPOINTS", + }), + ]); + + // Should call onSnapshotChange with deprecated = false + expect(onSnapshotChange).toHaveBeenCalledWith( + expect.objectContaining({ snapshot: expect.objectContaining({ friendlyId: "snapshot-3" }) }), + false + ); + + // Reset the mock + onSnapshotChange.mockClear(); + + // Simulate a series of snapshot changes with deprecation markers and a restored runner + // (standard checkpoint / restore flow) + runnerId = "test-runner-2"; + await manager.handleSnapshotChanges([ + createRunExecutionData({ snapshotId: "snapshot-4", executionStatus: "SUSPENDED" }), + createRunExecutionData({ snapshotId: "snapshot-5", executionStatus: "QUEUED" }), + createRunExecutionData({ snapshotId: "snapshot-6", executionStatus: "EXECUTING" }), + ]); + + // Should call onSnapshotChange with deprecated = false + expect(onSnapshotChange).toHaveBeenCalledWith( + expect.objectContaining({ snapshot: expect.objectContaining({ friendlyId: "snapshot-6" }) }), + false + ); + + // Reset the mock + onSnapshotChange.mockClear(); + + // Simulate a new snapshot with a deprecation marker in previous snapshots, but no restore + await manager.handleSnapshotChanges([ + createRunExecutionData({ snapshotId: "snapshot-7", executionStatus: "QUEUED" }), + createRunExecutionData({ snapshotId: "snapshot-8", executionStatus: "EXECUTING" }), + ]); + // Should call onSnapshotChange with deprecated = true + expect(onSnapshotChange).toHaveBeenCalledWith( + expect.objectContaining({ snapshot: expect.objectContaining({ friendlyId: "snapshot-8" }) }), + true + ); + }); }); // Helper to generate RunExecutionData with sensible defaults diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.ts index f109db17d0..885de3126d 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.ts @@ -2,26 +2,29 @@ import { tryCatch } from "@trigger.dev/core/utils"; import { RunLogger, SendDebugLogOptions } from "./logger.js"; import { TaskRunExecutionStatus, type RunExecutionData } from "@trigger.dev/core/v3"; import { assertExhaustive } from "@trigger.dev/core/utils"; +import { MetadataClient } from "./overrides.js"; export type SnapshotState = { id: string; status: TaskRunExecutionStatus; }; -type SnapshotHandler = (runData: RunExecutionData) => Promise; +type SnapshotHandler = (runData: RunExecutionData, deprecated: boolean) => Promise; type SuspendableHandler = (suspendableSnapshot: SnapshotState) => Promise; type SnapshotManagerOptions = { runFriendlyId: string; + runnerId: string; initialSnapshotId: string; initialStatus: TaskRunExecutionStatus; logger: RunLogger; + metadataClient?: MetadataClient; onSnapshotChange: SnapshotHandler; onSuspendable: SuspendableHandler; }; type QueuedChange = - | { id: string; type: "snapshot"; data: RunExecutionData } + | { id: string; type: "snapshot"; snapshots: RunExecutionData[] } | { id: string; type: "suspendable"; value: boolean }; type QueuedChangeItem = { @@ -31,10 +34,15 @@ type QueuedChangeItem = { }; export class SnapshotManager { - private state: SnapshotState; private runFriendlyId: string; + private runnerId: string; + private logger: RunLogger; + private metadataClient?: MetadataClient; + + private state: SnapshotState; private isSuspendable: boolean = false; + private readonly onSnapshotChange: SnapshotHandler; private readonly onSuspendable: SuspendableHandler; @@ -43,11 +51,16 @@ export class SnapshotManager { constructor(opts: SnapshotManagerOptions) { this.runFriendlyId = opts.runFriendlyId; + this.runnerId = opts.runnerId; + this.logger = opts.logger; + this.metadataClient = opts.metadataClient; + this.state = { id: opts.initialSnapshotId, status: opts.initialStatus, }; + this.onSnapshotChange = opts.onSnapshotChange; this.onSuspendable = opts.onSuspendable; } @@ -98,15 +111,15 @@ export class SnapshotManager { this.state = { id: snapshotId, status }; } - public async handleSnapshotChange(runData: RunExecutionData): Promise { - if (!this.statusCheck(runData)) { + public async handleSnapshotChanges(snapshots: RunExecutionData[]): Promise { + if (!this.statusCheck(snapshots)) { return; } return this.enqueueSnapshotChange({ id: crypto.randomUUID(), type: "snapshot", - data: runData, + snapshots, }); } @@ -114,8 +127,17 @@ export class SnapshotManager { return this.changeQueue.length; } - private statusCheck(runData: RunExecutionData): boolean { - const { run, snapshot } = runData; + private statusCheck(snapshots: RunExecutionData[]): boolean { + const latestSnapshot = snapshots[snapshots.length - 1]; + + if (!latestSnapshot) { + this.sendDebugLog("skipping status check for empty snapshots", { + snapshots, + }); + return false; + } + + const { run, snapshot } = latestSnapshot; const statusCheckData = { incomingId: snapshot.friendlyId, @@ -186,8 +208,17 @@ export class SnapshotManager { return -1; // a goes before b } if (a.change.type === "snapshot" && b.change.type === "snapshot") { - // sort snapshot changes by creation time, CUIDs are sortable - return a.change.data.snapshot.friendlyId.localeCompare(b.change.data.snapshot.friendlyId); + // sort snapshot changes by creation time of the latest snapshot, CUIDs are sortable + const aLatestSnapshot = a.change.snapshots[a.change.snapshots.length - 1]; + const bLatestSnapshot = b.change.snapshots[b.change.snapshots.length - 1]; + + if (!aLatestSnapshot || !bLatestSnapshot) { + return 0; + } + + return aLatestSnapshot.snapshot.friendlyId.localeCompare( + bLatestSnapshot.snapshot.friendlyId + ); } return 0; // both suspendable, maintain insertion order }); @@ -238,11 +269,40 @@ export class SnapshotManager { private async applyChange(change: QueuedChange): Promise { switch (change.type) { case "snapshot": { + const { snapshots } = change; + // Double check we should process this snapshot - if (!this.statusCheck(change.data)) { + if (!this.statusCheck(snapshots)) { return; } - const { snapshot } = change.data; + + const latestSnapshot = change.snapshots[change.snapshots.length - 1]; + if (!latestSnapshot) { + return; + } + + // These are the snapshots between the current and the latest one + const previousSnapshots = snapshots.slice(0, -1); + + // Check if any previous snapshot is QUEUED or SUSPENDED + const deprecatedStatus: TaskRunExecutionStatus[] = ["QUEUED", "SUSPENDED"]; + const deprecatedSnapshots = previousSnapshots.filter((snap) => + deprecatedStatus.includes(snap.snapshot.executionStatus) + ); + + let deprecated = false; + if (deprecatedSnapshots.length > 0) { + const hasBeenRestored = await this.hasBeenRestored(); + + if (hasBeenRestored) { + // It's normal for a restored run to have deprecation markers, e.g. it will have been SUSPENDED + deprecated = false; + } else { + deprecated = true; + } + } + + const { snapshot } = latestSnapshot; const oldState = { ...this.state }; this.updateSnapshot(snapshot.friendlyId, snapshot.executionStatus); @@ -252,10 +312,11 @@ export class SnapshotManager { newId: snapshot.friendlyId, oldStatus: oldState.status, newStatus: snapshot.executionStatus, + deprecated, }); // Execute handler - await this.onSnapshotChange(change.data); + await this.onSnapshotChange(latestSnapshot, deprecated); // Check suspendable state after snapshot change await this.checkSuspendableState(); @@ -274,6 +335,30 @@ export class SnapshotManager { } } + private async hasBeenRestored() { + if (!this.metadataClient) { + return false; + } + + const [error, overrides] = await this.metadataClient.getEnvOverrides(); + + if (error) { + return false; + } + + if (!overrides.TRIGGER_RUNNER_ID) { + return false; + } + + if (overrides.TRIGGER_RUNNER_ID === this.runnerId) { + return false; + } + + this.runnerId = overrides.TRIGGER_RUNNER_ID; + + return true; + } + private async checkSuspendableState() { if ( this.isSuspendable && From 2c8bcc2579bbffebcbdbd19e72a548f9bba5ac74 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 21:51:25 +0100 Subject: [PATCH 55/62] less logs --- packages/cli-v3/src/entryPoints/managed/execution.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 34a06a0da6..9b60a6ea62 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -1045,7 +1045,7 @@ export class RunExecution { this.sendDebugLog("handleSuspendable", { suspendableSnapshot }); if (!this.snapshotManager) { - this.sendDebugLog("handleSuspendable: missing snapshot manager"); + this.sendDebugLog("handleSuspendable: missing snapshot manager", { suspendableSnapshot }); return; } @@ -1147,7 +1147,6 @@ export class RunExecution { const { snapshots } = response.data; if (!snapshots.length) { - this.sendDebugLog(`fetchAndProcessSnapshotChanges: no new snapshots`, { source }); return; } From 1ce6ddcb3985096598b039a6674ef593a6921a93 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 22:00:16 +0100 Subject: [PATCH 56/62] rename snapshot manager stop --- packages/cli-v3/src/entryPoints/managed/execution.ts | 3 +-- packages/cli-v3/src/entryPoints/managed/snapshot.test.ts | 2 +- packages/cli-v3/src/entryPoints/managed/snapshot.ts | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 9b60a6ea62..81ef5693c0 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -1034,8 +1034,7 @@ export class RunExecution { this.shutdownReason = reason; this.snapshotPoller?.stop(); - this.snapshotManager?.dispose(); - + this.snapshotManager?.stop(); this.notifier?.stop(); this.taskRunProcess?.unsafeDetachEvtHandlers(); diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts index bf3b4668d8..538af7551d 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts @@ -313,7 +313,7 @@ describe("SnapshotManager", () => { expect(manager.queueLength).not.toBe(0); // Dispose manager before any promises complete - manager.dispose(); + manager.stop(); expect(manager.queueLength).toBe(0); diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.ts index 885de3126d..d69714e2db 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.ts @@ -371,12 +371,12 @@ export class SnapshotManager { } } - public dispose() { - this.sendDebugLog("dispose"); + public stop() { + this.sendDebugLog("stop"); // Clear any pending changes for (const item of this.changeQueue) { - item.reject(new Error("SnapshotManager disposed")); + item.reject(new Error("SnapshotManager stopped")); } this.changeQueue = []; } From 188a023178d73699ace1faea863bdf4599f538d6 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 6 May 2025 22:46:18 +0100 Subject: [PATCH 57/62] restore detection was moved into snapshot manager --- .../src/entryPoints/managed/execution.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed/execution.ts b/packages/cli-v3/src/entryPoints/managed/execution.ts index 81ef5693c0..41446cfa35 100644 --- a/packages/cli-v3/src/entryPoints/managed/execution.ts +++ b/packages/cli-v3/src/entryPoints/managed/execution.ts @@ -835,9 +835,7 @@ export class RunExecution { /** * Processes env overrides from the metadata service. Generally called when we're resuming from a suspended state. */ - public async processEnvOverrides( - reason?: string - ): Promise<{ executionWasRestored: boolean; overrides: Metadata } | null> { + public async processEnvOverrides(reason?: string): Promise<{ overrides: Metadata } | null> { if (!this.metadataClient) { return null; } @@ -866,20 +864,6 @@ export class RunExecution { currentEnv: this.env.raw, }); - let executionWasRestored = false; - - if (this.env.TRIGGER_RUNNER_ID !== overrides.TRIGGER_RUNNER_ID) { - this.sendDebugLog("[override] runner ID mismatch, execution was restored", { - reason, - currentRunnerId: this.env.TRIGGER_RUNNER_ID, - incomingRunnerId: overrides.TRIGGER_RUNNER_ID, - }); - - // we should keep a list of restored snapshots - - executionWasRestored = true; - } - // Override the env with the new values this.env.override(overrides); @@ -899,7 +883,6 @@ export class RunExecution { } return { - executionWasRestored, overrides, }; } From 4a27eea18d269b53f100fd495480d0b6cd798b12 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 7 May 2025 09:53:36 +0100 Subject: [PATCH 58/62] fix notifier logs --- packages/cli-v3/src/entryPoints/managed/notifier.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli-v3/src/entryPoints/managed/notifier.ts b/packages/cli-v3/src/entryPoints/managed/notifier.ts index 2685f81e6c..6f23c1b861 100644 --- a/packages/cli-v3/src/entryPoints/managed/notifier.ts +++ b/packages/cli-v3/src/entryPoints/managed/notifier.ts @@ -78,7 +78,8 @@ export class RunNotifier { } private sendDebugLog(message: string, properties?: SendDebugLogOptions["properties"]) { - this.logger?.sendDebugLog({ + this.logger.sendDebugLog({ + runId: this.runFriendlyId, message: `[notifier] ${message}`, properties: { ...properties, From 8c3da695eec43d8d4deb392f6f1b6557c5e6915e Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 7 May 2025 10:46:18 +0100 Subject: [PATCH 59/62] make runtime manager status a debug log --- packages/core/src/v3/runtime/sharedRuntimeManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/v3/runtime/sharedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts index acf734c05d..42f5092a7d 100644 --- a/packages/core/src/v3/runtime/sharedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -37,7 +37,7 @@ export class SharedRuntimeManager implements RuntimeManager { ) { // Log out the runtime status on a long interval to help debug stuck executions setInterval(() => { - this.log("[DEBUG] SharedRuntimeManager status", this.status); + this.debugLog("SharedRuntimeManager status", this.status); }, 300_000); } From bd8efa76548132cd4960e18870df1143b4b78dbc Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 7 May 2025 10:55:44 +0100 Subject: [PATCH 60/62] no need to attach runtime status twice --- packages/core/src/v3/runtime/sharedRuntimeManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/v3/runtime/sharedRuntimeManager.ts b/packages/core/src/v3/runtime/sharedRuntimeManager.ts index 42f5092a7d..a513e3a1d6 100644 --- a/packages/core/src/v3/runtime/sharedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/sharedRuntimeManager.ts @@ -37,7 +37,7 @@ export class SharedRuntimeManager implements RuntimeManager { ) { // Log out the runtime status on a long interval to help debug stuck executions setInterval(() => { - this.debugLog("SharedRuntimeManager status", this.status); + this.debugLog("SharedRuntimeManager status"); }, 300_000); } From 997185c7c943662e0e94eb4ad019c78c9d0ccbe5 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 7 May 2025 11:16:12 +0100 Subject: [PATCH 61/62] findUnique -> findFirst --- .../run-engine/src/engine/systems/executionSnapshotSystem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts index 52b354411c..98f5837d23 100644 --- a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts @@ -195,7 +195,7 @@ export async function getExecutionSnapshotsSince( sinceSnapshotId: string ): Promise { // Find the createdAt of the sinceSnapshotId - const sinceSnapshot = await prisma.taskRunExecutionSnapshot.findUnique({ + const sinceSnapshot = await prisma.taskRunExecutionSnapshot.findFirst({ where: { id: sinceSnapshotId }, select: { createdAt: true }, }); From 6769aeba2314be2f1b3f1b5607010d97bff53f6c Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 7 May 2025 12:14:12 +0100 Subject: [PATCH 62/62] sort snapshots by created at everywhere --- .../run-engine/src/engine/systems/dequeueSystem.ts | 1 + .../src/engine/systems/executionSnapshotSystem.ts | 2 ++ .../src/engine/systems/runAttemptSystem.ts | 1 + .../src/entryPoints/managed/snapshot.test.ts | 1 + .../cli-v3/src/entryPoints/managed/snapshot.ts | 14 ++++++-------- packages/core/src/v3/schemas/runEngine.ts | 1 + 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/internal-packages/run-engine/src/engine/systems/dequeueSystem.ts b/internal-packages/run-engine/src/engine/systems/dequeueSystem.ts index 9610c1accc..0e5800fa9a 100644 --- a/internal-packages/run-engine/src/engine/systems/dequeueSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/dequeueSystem.ts @@ -409,6 +409,7 @@ export class DequeueSystem { friendlyId: newSnapshot.friendlyId, executionStatus: newSnapshot.executionStatus, description: newSnapshot.description, + createdAt: newSnapshot.createdAt, }, image: result.deployment?.imageReference ?? undefined, checkpoint: newSnapshot.checkpoint ?? undefined, diff --git a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts index 98f5837d23..314fb23f8e 100644 --- a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts @@ -144,6 +144,7 @@ export function executionResultFromSnapshot(snapshot: TaskRunExecutionSnapshot): friendlyId: SnapshotId.toFriendlyId(snapshot.id), executionStatus: snapshot.executionStatus, description: snapshot.description, + createdAt: snapshot.createdAt, }, run: { id: snapshot.runId, @@ -162,6 +163,7 @@ export function executionDataFromSnapshot(snapshot: EnhancedExecutionSnapshot): friendlyId: snapshot.friendlyId, executionStatus: snapshot.executionStatus, description: snapshot.description, + createdAt: snapshot.createdAt, }, run: { id: snapshot.runId, diff --git a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts index 0ee1af5576..2150f70ac9 100644 --- a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts @@ -892,6 +892,7 @@ export class RunAttemptSystem { friendlyId: newSnapshot.friendlyId, executionStatus: newSnapshot.executionStatus, description: newSnapshot.description, + createdAt: newSnapshot.createdAt, }, run: { id: newSnapshot.runId, diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts index 538af7551d..05cba11f38 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.test.ts @@ -728,6 +728,7 @@ function createRunExecutionData( friendlyId: snapshotFriendlyId, executionStatus: overrides.executionStatus ?? "EXECUTING", description: overrides.description ?? "Test snapshot", + createdAt: new Date(), }, completedWaitpoints: [], }; diff --git a/packages/cli-v3/src/entryPoints/managed/snapshot.ts b/packages/cli-v3/src/entryPoints/managed/snapshot.ts index d69714e2db..75d3d4b036 100644 --- a/packages/cli-v3/src/entryPoints/managed/snapshot.ts +++ b/packages/cli-v3/src/entryPoints/managed/snapshot.ts @@ -199,7 +199,7 @@ export class SnapshotManager { // Sort queue: // 1. Suspendable changes always go to the back - // 2. Snapshot changes are ordered by ID + // 2. Snapshot changes are ordered by creation time, with the latest snapshot last this.changeQueue.sort((a, b) => { if (a.change.type === "suspendable" && b.change.type === "snapshot") { return 1; // a goes after b @@ -208,17 +208,15 @@ export class SnapshotManager { return -1; // a goes before b } if (a.change.type === "snapshot" && b.change.type === "snapshot") { - // sort snapshot changes by creation time of the latest snapshot, CUIDs are sortable - const aLatestSnapshot = a.change.snapshots[a.change.snapshots.length - 1]; - const bLatestSnapshot = b.change.snapshots[b.change.snapshots.length - 1]; + const snapshotA = a.change.snapshots[a.change.snapshots.length - 1]; + const snapshotB = b.change.snapshots[b.change.snapshots.length - 1]; - if (!aLatestSnapshot || !bLatestSnapshot) { + if (!snapshotA || !snapshotB) { return 0; } - return aLatestSnapshot.snapshot.friendlyId.localeCompare( - bLatestSnapshot.snapshot.friendlyId - ); + // Sort snapshot changes by creation time, old -> new + return snapshotA.snapshot.createdAt.getTime() - snapshotB.snapshot.createdAt.getTime(); } return 0; // both suspendable, maintain insertion order }); diff --git a/packages/core/src/v3/schemas/runEngine.ts b/packages/core/src/v3/schemas/runEngine.ts index efcfe8086e..2dd75e0ddd 100644 --- a/packages/core/src/v3/schemas/runEngine.ts +++ b/packages/core/src/v3/schemas/runEngine.ts @@ -109,6 +109,7 @@ const ExecutionSnapshot = z.object({ friendlyId: z.string(), executionStatus: z.enum(Object.values(TaskRunExecutionStatus) as [TaskRunExecutionStatus]), description: z.string(), + createdAt: z.coerce.date(), }); const BaseRunMetadata = z.object({