From 99f0787148334afe2d2a007ce196e01eceaaae86 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 18 Aug 2026 07:23:52 +0100 Subject: [PATCH 01/98] feat(cli,webapp): default new projects to node-24 (#4649) --- .changeset/node-24-project-default.md | 5 ++++ apps/webapp/app/models/project.server.ts | 1 + .../app/models/runtimeEnvironment.server.ts | 2 ++ .../api.v1.projects.$projectRef.$env.ts | 4 ++- .../app/routes/api.v1.projects.$projectRef.ts | 4 ++- .../services/initializeDeployment.server.ts | 2 +- .../projectEnvironmentCredentialRoute.test.ts | 2 ++ .../migration.sql | 2 ++ .../database/prisma/schema.prisma | 27 ++++++++++--------- packages/cli-v3/src/commands/deploy.ts | 6 ++++- packages/cli-v3/src/commands/init.ts | 6 ++--- packages/cli-v3/src/config.test.ts | 20 +++++++++++++- packages/cli-v3/src/config.ts | 27 +++++++++++++------ packages/cli-v3/src/utilities/session.ts | 1 + packages/core/src/v3/auth/environment.ts | 1 + packages/core/src/v3/schemas/api.ts | 3 +++ 16 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 .changeset/node-24-project-default.md create mode 100644 internal-packages/database/prisma/migrations/20260817111521_add_project_default_runtime/migration.sql diff --git a/.changeset/node-24-project-default.md b/.changeset/node-24-project-default.md new file mode 100644 index 00000000000..551a7d5a23f --- /dev/null +++ b/.changeset/node-24-project-default.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +New projects created with `trigger init` use Node.js 24 by default. Deployments without explicit `runtime` now use their project's configured default runtime. diff --git a/apps/webapp/app/models/project.server.ts b/apps/webapp/app/models/project.server.ts index 2ed317fe879..f309ae3212d 100644 --- a/apps/webapp/app/models/project.server.ts +++ b/apps/webapp/app/models/project.server.ts @@ -113,6 +113,7 @@ export async function createProject( // for historical rows; the V1->V2 upgrade guards on worker-register / deploy // stay in place to migrate existing legacy projects. engine: "V2", + defaultRuntime: "node-24", onboardingData, }, include: { diff --git a/apps/webapp/app/models/runtimeEnvironment.server.ts b/apps/webapp/app/models/runtimeEnvironment.server.ts index 8dc5a68b63f..e7cf10f3e02 100644 --- a/apps/webapp/app/models/runtimeEnvironment.server.ts +++ b/apps/webapp/app/models/runtimeEnvironment.server.ts @@ -6,6 +6,7 @@ import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver. import { logger } from "~/services/logger.server"; import { getUsername } from "~/utils/username"; import { hashApiKey } from "~/utils/apiKeys"; +import { BuildRuntime } from "@trigger.dev/core/v3"; import { isAdditionalApiKey } from "@trigger.dev/core/v3/apiKeys"; import { isDefaultDevBranch, sanitizeBranchName } from "@trigger.dev/core/v3/utils/gitBranch"; import { scopesGrantFullAccess } from "@trigger.dev/rbac"; @@ -77,6 +78,7 @@ export function toAuthenticated( defaultWorkerGroupId: env.project.defaultWorkerGroupId, organizationId: env.project.organizationId, builderProjectId: env.project.builderProjectId, + defaultRuntime: BuildRuntime.nullable().safeParse(env.project.defaultRuntime).data ?? null, }, organization: { id: env.organization.id, diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts index c7090ce4721..8083ba1a0a4 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts @@ -1,5 +1,5 @@ import { json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { type GetProjectEnvResponse } from "@trigger.dev/core/v3"; +import { BuildRuntime, type GetProjectEnvResponse } from "@trigger.dev/core/v3"; import { z } from "zod"; import { env as processEnv } from "~/env.server"; import { @@ -65,6 +65,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { name: environment.project.name, apiUrl: processEnv.API_ORIGIN ?? processEnv.APP_ORIGIN, projectId: environment.project.id, + defaultRuntime: + BuildRuntime.nullable().safeParse(environment.project.defaultRuntime ?? null).data ?? null, }; return json(result); diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.ts index c1fa0acc917..824ab1199a8 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.ts @@ -1,5 +1,5 @@ import { json } from "@remix-run/server-runtime"; -import type { GetProjectResponseBody } from "@trigger.dev/core/v3"; +import { BuildRuntime, type GetProjectResponseBody } from "@trigger.dev/core/v3"; import { z } from "zod"; import { prisma } from "~/db.server"; import { DeleteProjectService } from "~/services/deleteProject.server"; @@ -53,6 +53,8 @@ export const loader = createLoaderPATApiRoute( slug: project.slug, createdAt: project.createdAt, defaultRegion: project.defaultWorkerGroup?.name ?? null, + defaultRuntime: + BuildRuntime.nullable().safeParse(project.defaultRuntime ?? null).data ?? null, organization: { id: project.organization.id, title: project.organization.title, diff --git a/apps/webapp/app/v3/services/initializeDeployment.server.ts b/apps/webapp/app/v3/services/initializeDeployment.server.ts index abb99082dd6..fb8ffd259e4 100644 --- a/apps/webapp/app/v3/services/initializeDeployment.server.ts +++ b/apps/webapp/app/v3/services/initializeDeployment.server.ts @@ -253,7 +253,7 @@ export class InitializeDeploymentService extends BaseService { imagePlatform: env.DEPLOY_IMAGE_PLATFORM, git: payload.gitMeta ?? undefined, commitSHA: payload.gitMeta?.commitSha ?? undefined, - runtime: payload.runtime ?? undefined, + runtime: payload.runtime ?? environment.project.defaultRuntime ?? undefined, triggeredVia: payload.triggeredVia ?? undefined, startedAt: initialStatus === "BUILDING" ? new Date() : undefined, }; diff --git a/apps/webapp/test/projectEnvironmentCredentialRoute.test.ts b/apps/webapp/test/projectEnvironmentCredentialRoute.test.ts index 9a24883a85a..e50363c71dc 100644 --- a/apps/webapp/test/projectEnvironmentCredentialRoute.test.ts +++ b/apps/webapp/test/projectEnvironmentCredentialRoute.test.ts @@ -37,6 +37,7 @@ const environment = { project: { id: "proj_123", name: "Example project", + defaultRuntime: "node-24", }, }; @@ -82,6 +83,7 @@ describe("project environment credential response", () => { await expect(responseJson(response)).resolves.toMatchObject({ apiKey: "tr_prod_sk_presented", projectId: "proj_123", + defaultRuntime: "node-24", }); expect(mocks.authorizePatEnvironmentAccess).not.toHaveBeenCalled(); }); diff --git a/internal-packages/database/prisma/migrations/20260817111521_add_project_default_runtime/migration.sql b/internal-packages/database/prisma/migrations/20260817111521_add_project_default_runtime/migration.sql new file mode 100644 index 00000000000..899dd5921a4 --- /dev/null +++ b/internal-packages/database/prisma/migrations/20260817111521_add_project_default_runtime/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "defaultRuntime" TEXT; diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 44392406aae..9c45d7fa7c0 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -474,6 +474,9 @@ model Project { /// Set the first time the CLI `init` command completes against this project. Drives the dev onboarding progress. initializedAt DateTime? + /// Runtime used when a deployment config does not specify one. Null preserves the legacy Node 20 fallback. + defaultRuntime String? + version ProjectVersion @default(V2) engine RunEngineVersion @default(V1) @@ -790,12 +793,12 @@ model WebhookEndpoint { source String // provider tag e.g. "stripe","slack","github" handlerWebhookId String // declared webhook() id (string ref, GOLDEN LAW, no relation) - routingTarget Json // RoutingTarget tagged union ({ type: "task" } | { type: "session" }) - verifierArtifact Json // VerifierArtifact tagged union (config|preset in P1) + routingTarget Json // RoutingTarget tagged union ({ type: "task" } | { type: "session" }) + verifierArtifact Json // VerifierArtifact tagged union (config|preset in P1) filter String? // source filter DSL string (display/round-trip) - filterAst Json? // compiled FilterAst, evaluated at ingest; null = route all - filterAstVersion Int? // re-parse `filter` on a format bump - metadata Json @default("{}") // arbitrary user metadata; flows into the webhook task + filterAst Json? // compiled FilterAst, evaluated at ingest; null = route all + filterAstVersion Int? // re-parse `filter` on a format bump + metadata Json @default("{}") // arbitrary user metadata; flows into the webhook task // who supplies the secret/key; drives the Connect UI (paste vs generate). From the source. secretProvisioning String @default("either") // "provider" | "integrator" | "either" @@ -803,13 +806,13 @@ model WebhookEndpoint { /// SecretReference.key string. Plain String, NO @relation -> no FK to SecretReference. signingSecretKey String? - status WebhookEndpointStatus @default(ACTIVE) + status WebhookEndpointStatus @default(ACTIVE) /// When an operator disabled the endpoint via the dashboard/API. Null means the declarative sync /// owns the status: a redeploy that re-declares a previously-removed (auto-deactivated) webhook /// reactivates it. Non-null means the operator disabled it, so the sync leaves the status alone. manuallyDeactivatedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@unique([runtimeEnvironmentId, handlerWebhookId, endpointTenantId, endpointExternalRef]) // deploy-sync key @@index([runtimeEnvironmentId, source]) @@ -842,8 +845,8 @@ model WebhookDelivery { /// Set from the x-trigger-test ingress header; marks console/test-send deliveries so the list can filter them. isTest Boolean @default(false) - parsedEvent Json? // size-capped snapshot of the verified event (full event lives in ClickHouse) - headers Json? // inbound request headers, surfaced to the webhook task via onEvent({ headers }) + parsedEvent Json? // size-capped snapshot of the verified event (full event lives in ClickHouse) + headers Json? // inbound request headers, surfaced to the webhook task via onEvent({ headers }) rawBodyHash String? // sha256 of raw bytes; cheap P2 replay anchor errorMessage String? filterReason String? // why a FILTERED delivery was not routed (failing clause + actual value) @@ -2370,8 +2373,8 @@ model TaskSchedule { timezone String @default("UTC") // Cron spread - windowDurationSeconds Int? - windowPercentage Int? + windowDurationSeconds Int? + windowPercentage Int? ///Can be provided by the user then accessed inside a run externalId String? diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index f82942d4e43..41c48330836 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -300,7 +300,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { logger.debug("Using project ref from env", { ref: envVars.TRIGGER_PROJECT_REF }); } - const resolvedConfig = await loadConfig({ + let resolvedConfig = await loadConfig({ cwd: projectPath, overrides: { project: options.projectRef ?? envVars.TRIGGER_PROJECT_REF }, configFile: options.config, @@ -364,6 +364,10 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { throw new Error("Failed to get project client"); } + if (!resolvedConfig.runtimeWasExplicit && projectClient.defaultRuntime) { + resolvedConfig.runtime = projectClient.defaultRuntime; + } + if (options.nativeBuildServer) { await handleNativeBuildServerDeploy({ apiClient: projectClient.client, diff --git a/packages/cli-v3/src/commands/init.ts b/packages/cli-v3/src/commands/init.ts index a6ae5af60cb..46aeee5f5e0 100644 --- a/packages/cli-v3/src/commands/init.ts +++ b/packages/cli-v3/src/commands/init.ts @@ -50,7 +50,7 @@ const InitCommandOptions = CommonCommandOptions.extend({ overrideConfig: z.boolean().default(false), tag: z.string().default(cliVersion), skipPackageInstall: z.boolean().default(false), - runtime: z.string().default("node"), + runtime: z.string().default("node-24"), pkgArgs: z.string().optional(), gitRef: z.string().default("main"), javascript: z.boolean().default(false), @@ -94,8 +94,8 @@ Examples: ) .option( "-r, --runtime ", - "Which runtime to use for the project. Supported: node, node-22, bun", - "node" + "Which runtime to use for the project. Supported: node, node-22, node-24, node-26, bun", + "node-24" ) .option("--skip-package-install", "Skip installing the @trigger.dev/sdk package") .option("--override-config", "Override the existing config file if it exists") diff --git a/packages/cli-v3/src/config.test.ts b/packages/cli-v3/src/config.test.ts index cafee4b9a3d..6a77348f876 100644 --- a/packages/cli-v3/src/config.test.ts +++ b/packages/cli-v3/src/config.test.ts @@ -45,7 +45,25 @@ describe("loadConfig runtime", () => { await expect(loadConfig({ cwd, warn: false })).resolves.toMatchObject({ runtime: expected }); }); - it("keeps node as the default", async () => { + it("tracks whether runtime was explicitly configured", async () => { + const cwd = await createProject("node-22"); + + await expect(loadConfig({ cwd, warn: false })).resolves.toMatchObject({ + runtime: "node-22", + runtimeWasExplicit: true, + }); + }); + + it("tracks an omitted runtime separately from the legacy default", async () => { + const cwd = await createProject(); + + await expect(loadConfig({ cwd, warn: false })).resolves.toMatchObject({ + runtime: "node", + runtimeWasExplicit: false, + }); + }); + + it("keeps node as the legacy default when runtime is omitted", async () => { const cwd = await createProject(); await expect(loadConfig({ cwd, warn: false })).resolves.toMatchObject({ runtime: "node" }); diff --git a/packages/cli-v3/src/config.ts b/packages/cli-v3/src/config.ts index 2e7ffdee509..9f3c13413bf 100644 --- a/packages/cli-v3/src/config.ts +++ b/packages/cli-v3/src/config.ts @@ -37,12 +37,16 @@ export type ResolveConfigOptions = { warn?: boolean; }; +export type LoadedConfig = ResolvedConfig & { + runtimeWasExplicit: boolean; +}; + export async function loadConfig({ cwd = process.cwd(), overrides, configFile, warn = true, -}: ResolveConfigOptions = {}): Promise { +}: ResolveConfigOptions = {}): Promise { const result = await c12.loadConfig({ name: "trigger", cwd, @@ -54,13 +58,13 @@ export async function loadConfig({ } type ResolveWatchConfigOptions = ResolveConfigOptions & { - onUpdate: (config: ResolvedConfig) => void; + onUpdate: (config: LoadedConfig) => void; debounce?: number; ignoreInitial?: boolean; }; type ResolveWatchConfigResult = { - config: ResolvedConfig; + config: LoadedConfig; files: string[]; stop: () => Promise; }; @@ -157,7 +161,7 @@ async function resolveConfig( result: c12.ResolvedConfig, overrides?: Partial, warn = true -): Promise { +): Promise { // `trigger.config` is the fallback value set by c12. Bail out with actionable guidance before // touching the filesystem: the pkg-types resolvers below throw raw errors when run outside a // project (e.g. `dev` before `init`), which would mask this message. @@ -181,8 +185,8 @@ async function resolveConfig( const features = featuresFromCompatibilityFlags( ["run_engine_v2" as const].concat(config.compatibilityFlags ?? []) ); - const defaultRuntime: BuildRuntime = features.run_engine_v2 ? "node" : DEFAULT_RUNTIME; - const configuredRuntime = overrides?.runtime ?? config.runtime ?? defaultRuntime; + const legacyDefaultRuntime: BuildRuntime = features.run_engine_v2 ? "node" : DEFAULT_RUNTIME; + const configuredRuntime = overrides?.runtime ?? config.runtime ?? legacyDefaultRuntime; const runtime = resolveBuildRuntime(configuredRuntime); if (warn && isDeprecatedConfigRuntime(configuredRuntime)) { @@ -224,7 +228,7 @@ async function resolveConfig( config, { dirs, - runtime: defaultRuntime, + runtime: legacyDefaultRuntime, tsconfig: tsconfigPath, build: { jsx: { @@ -241,12 +245,19 @@ async function resolveConfig( } ) as ResolvedConfig; // TODO: For some reason, without this, there is a weird type error complaining about tsconfigPath being string | nullish, which can't be assigned to string | undefined - return { + const resolvedConfig = { ...mergedConfig, dirs: Array.from(new Set(dirs)), instrumentedPackageNames: getInstrumentedPackageNames(mergedConfig), runtime, }; + + Object.defineProperty(resolvedConfig, "runtimeWasExplicit", { + value: overrides?.runtime !== undefined || config.runtime !== undefined, + enumerable: false, + }); + + return resolvedConfig as LoadedConfig; } function resolveTriggerDir(dir: string, workingDir: string): string { diff --git a/packages/cli-v3/src/utilities/session.ts b/packages/cli-v3/src/utilities/session.ts index b7583f83c9f..2500cf1f368 100644 --- a/packages/cli-v3/src/utilities/session.ts +++ b/packages/cli-v3/src/utilities/session.ts @@ -112,6 +112,7 @@ export async function getProjectClient(options: GetEnvOptions) { return { id: projectEnv.data.projectId, name: projectEnv.data.name, + defaultRuntime: projectEnv.data.defaultRuntime, client, }; } diff --git a/packages/core/src/v3/auth/environment.ts b/packages/core/src/v3/auth/environment.ts index 498393722fe..f61a23be28d 100644 --- a/packages/core/src/v3/auth/environment.ts +++ b/packages/core/src/v3/auth/environment.ts @@ -67,6 +67,7 @@ export type AuthenticatedEnvironment = { // Build-server bookkeeping. Read by remote-image-builder when // creating Depot builds. builderProjectId: string | null; + defaultRuntime?: string | null; }; organization: { diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 6cd100f7c3c..12e991cfef9 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -11,6 +11,7 @@ import { BackgroundWorkerMetadata } from "./resources.js"; import { DequeuedMessage, MachineResources } from "./runEngine.js"; import { QueueTypeName } from "./queues.js"; import { ScheduleWindow } from "./schemas.js"; +import { BuildRuntime } from "./build.js"; export const RunEngineVersion = z.union([z.literal("V1"), z.literal("V2")]); @@ -43,6 +44,7 @@ export const GetProjectResponseBody = z.object({ // (the project falls back to the global platform default). Optional so a // newer client still parses responses from an older server that omits it. defaultRegion: z.string().nullable().optional(), + defaultRuntime: BuildRuntime.nullable().optional(), organization: z.object({ id: z.string(), title: z.string(), @@ -98,6 +100,7 @@ export const GetProjectEnvResponse = z.object({ name: z.string(), apiUrl: z.string(), projectId: z.string(), + defaultRuntime: BuildRuntime.nullable().optional(), }); export type GetProjectEnvResponse = z.infer; From a55f7cdf4d136c2231b62960a7c97e0a9fc3ebdb Mon Sep 17 00:00:00 2001 From: Wes Mason Date: Tue, 18 Aug 2026 09:36:48 +0100 Subject: [PATCH 02/98] fix(run-engine): stop a '*' concurrency key stranding its whole base queue (#4628) ## The bug A concurrency key is an unrestricted client string (`ConcurrencyKeySchema` is `z.union([z.string(), z.number()]).transform(String)`), and `concurrencyKeySection` does no escaping, so `*` reaches the queue raw. `queueKey` then renders it as `...:queue::ck:*`, which is byte-identical to the wildcard member the CK scripts keep in the master queue to mean "this base queue has concurrency-key work". Every CK script ends with the same pair: ```lua -- Rebalance master queue with ck:* member redis.call('ZADD', masterQueueKey, earliestIdx[2], ckWildcardName) -- Remove old-format entry from master queue (transition cleanup) redis.call('ZREM', masterQueueKey, queueName) ``` `ckWildcardName` is `toCkWildcard(message.queue)`, and for a `*`-keyed run that returns the identical string, so the cleanup on the second line deletes what the rebalance on the first line just wrote. The master queue then has no entry for that base queue, while `ckIndex` and the variant queues still hold the work. **Every concurrency key on the queue stops being dequeued**, not just the `*` one. It is silent, and it only recovers if some later write happens to re-add the member. Reproduced before the fix: ``` master queue AFTER normal ck enqueue: ["{org:...}:queue:task/my-task:ck:*"] master queue AFTER ck='*' enqueue: [] ckIndex members (work still queued): [":ck:user-1", ":ck:*"] dequeued: [] ``` Blast radius is bounded to the environment that triggers it, so it is self-inflicted rather than cross-tenant, but a single trigger stalls the queue for everything on it. ## The fix Guard the cleanup so it never removes the wildcard member: ```lua if queueName ~= ckWildcardName then redis.call('ZREM', masterQueueKey, queueName) end ``` Applied to all 10 CK scripts (4 enqueue, 6 ack/nack/dead-letter). No key-format change and no migration: a queue already stranded in Redis is repaired by its next write. I considered rejecting `*` at the API boundary instead and rejected it. Existing Redis state and `TaskRun.concurrencyKey` rows already hold raw `:`-bearing and `*` keys, so changing key construction would orphan in-flight messages and split concurrency accounting mid-deploy. Boundary validation would still be reasonable as belt-and-braces later, but the Lua guard alone fixes it including for state already out there. ## Testing `ckWildcardKey.test.ts` covers the enqueue, ack and nack paths. All three pass with the guard and **all three fail without it**, verified by reverting. Full `src/run-queue/` suite is green (166 tests). ## Note for #4367 The virtual-time branch adds three more CK scripts with the same pattern (`enqueueMessageCkVtimeTracked`, `enqueueMessageWithTtlCkVtimeTracked`, `nackMessageCkVtimeTracked`). They do not exist on main so they are not in this PR; the same guard needs applying there, and I will do that on that branch. --- .server-changes/ck-wildcard-queue-strand.md | 6 + .../run-engine/src/run-queue/index.ts | 90 +++++++-- .../src/run-queue/tests/ckWildcardKey.test.ts | 190 ++++++++++++++++++ 3 files changed, 266 insertions(+), 20 deletions(-) create mode 100644 .server-changes/ck-wildcard-queue-strand.md create mode 100644 internal-packages/run-engine/src/run-queue/tests/ckWildcardKey.test.ts diff --git a/.server-changes/ck-wildcard-queue-strand.md b/.server-changes/ck-wildcard-queue-strand.md new file mode 100644 index 00000000000..67642e4cb0c --- /dev/null +++ b/.server-changes/ck-wildcard-queue-strand.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Using `*` as a concurrency key no longer stops a queue from being processed. Triggering a single run with that key could leave the whole queue stalled, including runs using other concurrency keys on it, until something else was triggered on the same queue. diff --git a/internal-packages/run-engine/src/run-queue/index.ts b/internal-packages/run-engine/src/run-queue/index.ts index cd6a8ce3bd8..b5a7eba25af 100644 --- a/internal-packages/run-engine/src/run-queue/index.ts +++ b/internal-packages/run-engine/src/run-queue/index.ts @@ -3603,8 +3603,13 @@ if #earliestIdx > 0 then redis.call('ZADD', masterQueueKey, earliestIdx[2], ckWildcardName) end --- Remove old-format entry from master queue (transition cleanup) -redis.call('ZREM', masterQueueKey, queueName) +-- Remove old-format entry from master queue (transition cleanup). Skipped when the +-- variant name IS the wildcard: a concurrency key of '*' produces a queue key identical +-- to the wildcard member, so an unguarded ZREM here deletes the entry the rebalance just +-- wrote and strands every concurrency key on this base queue. +if queueName ~= ckWildcardName then + redis.call('ZREM', masterQueueKey, queueName) +end -- Update the concurrency keys redis.call('SREM', queueCurrentConcurrencyKey, messageId) @@ -3708,8 +3713,13 @@ if #earliestIdx > 0 then redis.call('ZADD', masterQueueKey, earliestIdx[2], ckWildcardName) end --- Remove old-format entry from master queue (transition cleanup) -redis.call('ZREM', masterQueueKey, queueName) +-- Remove old-format entry from master queue (transition cleanup). Skipped when the +-- variant name IS the wildcard: a concurrency key of '*' produces a queue key identical +-- to the wildcard member, so an unguarded ZREM here deletes the entry the rebalance just +-- wrote and strands every concurrency key on this base queue. +if queueName ~= ckWildcardName then + redis.call('ZREM', masterQueueKey, queueName) +end -- Update the concurrency keys redis.call('SREM', queueCurrentConcurrencyKey, messageId) @@ -3838,8 +3848,13 @@ if #earliestIdx > 0 then redis.call('ZADD', masterQueueKey, earliestIdx[2], ckWildcardName) end --- Remove old-format entry from master queue (transition cleanup) -redis.call('ZREM', masterQueueKey, queueName) +-- Remove old-format entry from master queue (transition cleanup). Skipped when the +-- variant name IS the wildcard: a concurrency key of '*' produces a queue key identical +-- to the wildcard member, so an unguarded ZREM here deletes the entry the rebalance just +-- wrote and strands every concurrency key on this base queue. +if queueName ~= ckWildcardName then + redis.call('ZREM', masterQueueKey, queueName) +end -- Update the concurrency keys redis.call('SREM', queueCurrentConcurrencyKey, messageId) @@ -3956,8 +3971,13 @@ if #earliestIdx > 0 then redis.call('ZADD', masterQueueKey, earliestIdx[2], ckWildcardName) end --- Remove old-format entry from master queue (transition cleanup) -redis.call('ZREM', masterQueueKey, queueName) +-- Remove old-format entry from master queue (transition cleanup). Skipped when the +-- variant name IS the wildcard: a concurrency key of '*' produces a queue key identical +-- to the wildcard member, so an unguarded ZREM here deletes the entry the rebalance just +-- wrote and strands every concurrency key on this base queue. +if queueName ~= ckWildcardName then + redis.call('ZREM', masterQueueKey, queueName) +end -- Update the concurrency keys redis.call('SREM', queueCurrentConcurrencyKey, messageId) @@ -4908,8 +4928,13 @@ else redis.call('ZADD', masterQueueKey, earliestInCkIndex[2], ckWildcardName) end --- Remove old-format entry from master queue (transition cleanup) -redis.call('ZREM', masterQueueKey, messageQueueName) +-- Remove old-format entry from master queue (transition cleanup). Skipped when the +-- variant name IS the wildcard: a concurrency key of '*' produces a queue key identical +-- to the wildcard member, so an unguarded ZREM here deletes the entry the rebalance just +-- wrote and strands every concurrency key on this base queue. +if messageQueueName ~= ckWildcardName then + redis.call('ZREM', masterQueueKey, messageQueueName) +end -- Update the concurrency keys redis.call('SREM', queueCurrentConcurrencyKey, messageId) @@ -4973,8 +4998,13 @@ else redis.call('ZADD', masterQueueKey, earliestIdx[2], ckWildcardName) end --- Remove old-format entry from master queue (transition cleanup) -redis.call('ZREM', masterQueueKey, messageQueueName) +-- Remove old-format entry from master queue (transition cleanup). Skipped when the +-- variant name IS the wildcard: a concurrency key of '*' produces a queue key identical +-- to the wildcard member, so an unguarded ZREM here deletes the entry the rebalance just +-- wrote and strands every concurrency key on this base queue. +if messageQueueName ~= ckWildcardName then + redis.call('ZREM', masterQueueKey, messageQueueName) +end `, }); @@ -5019,8 +5049,13 @@ else redis.call('ZADD', masterQueueKey, earliestIdx[2], ckWildcardName) end --- Remove old-format entry from master queue (transition cleanup) -redis.call('ZREM', masterQueueKey, messageQueueName) +-- Remove old-format entry from master queue (transition cleanup). Skipped when the +-- variant name IS the wildcard: a concurrency key of '*' produces a queue key identical +-- to the wildcard member, so an unguarded ZREM here deletes the entry the rebalance just +-- wrote and strands every concurrency key on this base queue. +if messageQueueName ~= ckWildcardName then + redis.call('ZREM', masterQueueKey, messageQueueName) +end -- Add the message to the dead letter queue redis.call('ZADD', deadLetterQueueKey, tonumber(redis.call('TIME')[1]), messageId) @@ -5095,8 +5130,13 @@ else redis.call('ZADD', masterQueueKey, earliestInCkIndex[2], ckWildcardName) end --- Remove old-format entry from master queue (transition cleanup) -redis.call('ZREM', masterQueueKey, messageQueueName) +-- Remove old-format entry from master queue (transition cleanup). Skipped when the +-- variant name IS the wildcard: a concurrency key of '*' produces a queue key identical +-- to the wildcard member, so an unguarded ZREM here deletes the entry the rebalance just +-- wrote and strands every concurrency key on this base queue. +if messageQueueName ~= ckWildcardName then + redis.call('ZREM', masterQueueKey, messageQueueName) +end -- Update the concurrency keys. DECR runningCounter only when SREM -- currentDequeued actually removed an entry (the message was in flight). @@ -5201,8 +5241,13 @@ else redis.call('ZADD', masterQueueKey, earliestIdx[2], ckWildcardName) end --- Remove old-format entry from master queue (transition cleanup) -redis.call('ZREM', masterQueueKey, messageQueueName) +-- Remove old-format entry from master queue (transition cleanup). Skipped when the +-- variant name IS the wildcard: a concurrency key of '*' produces a queue key identical +-- to the wildcard member, so an unguarded ZREM here deletes the entry the rebalance just +-- wrote and strands every concurrency key on this base queue. +if messageQueueName ~= ckWildcardName then + redis.call('ZREM', masterQueueKey, messageQueueName) +end `, }); @@ -5261,8 +5306,13 @@ else redis.call('ZADD', masterQueueKey, earliestIdx[2], ckWildcardName) end --- Remove old-format entry from master queue (transition cleanup) -redis.call('ZREM', masterQueueKey, messageQueueName) +-- Remove old-format entry from master queue (transition cleanup). Skipped when the +-- variant name IS the wildcard: a concurrency key of '*' produces a queue key identical +-- to the wildcard member, so an unguarded ZREM here deletes the entry the rebalance just +-- wrote and strands every concurrency key on this base queue. +if messageQueueName ~= ckWildcardName then + redis.call('ZREM', masterQueueKey, messageQueueName) +end -- Add the message to the dead letter queue redis.call('ZADD', deadLetterQueueKey, tonumber(redis.call('TIME')[1]), messageId) diff --git a/internal-packages/run-engine/src/run-queue/tests/ckWildcardKey.test.ts b/internal-packages/run-engine/src/run-queue/tests/ckWildcardKey.test.ts new file mode 100644 index 00000000000..780573f6664 --- /dev/null +++ b/internal-packages/run-engine/src/run-queue/tests/ckWildcardKey.test.ts @@ -0,0 +1,190 @@ +import { redisTest } from "@internal/testcontainers"; +import { trace } from "@internal/tracing"; +import { Logger } from "@trigger.dev/core/logger"; +import { Decimal } from "@trigger.dev/database"; +import { FairQueueSelectionStrategy } from "../fairQueueSelectionStrategy.js"; +import { RunQueue } from "../index.js"; +import { RunQueueFullKeyProducer } from "../keyProducer.js"; +import type { InputPayload } from "../types.js"; + +const testOptions = { + name: "rq", + tracer: trace.getTracer("rq"), + workers: 1, + defaultEnvConcurrency: 25, + logger: new Logger("RunQueue", "warn"), + retryOptions: { + maxAttempts: 5, + factor: 1.1, + minTimeoutInMs: 100, + maxTimeoutInMs: 1_000, + randomize: true, + }, + keys: new RunQueueFullKeyProducer(), +}; + +const authenticatedEnvDev = { + id: "e1234", + type: "DEVELOPMENT" as const, + maximumConcurrencyLimit: 10, + concurrencyLimitBurstFactor: new Decimal(2.0), + project: { id: "p1234" }, + organization: { id: "o1234" }, +}; + +function createQueue(redisContainer: any) { + return new RunQueue({ + ...testOptions, + masterQueueConsumersDisabled: true, + workerOptions: { disabled: true }, + queueSelectionStrategy: new FairQueueSelectionStrategy({ + redis: { + keyPrefix: "runqueue:test:", + host: redisContainer.getHost(), + port: redisContainer.getPort(), + }, + keys: testOptions.keys, + }), + redis: { + keyPrefix: "runqueue:test:", + host: redisContainer.getHost(), + port: redisContainer.getPort(), + }, + }); +} + +function makeMessage(overrides: Partial = {}): InputPayload { + return { + runId: "r1", + taskIdentifier: "task/my-task", + orgId: "o1234", + projectId: "p1234", + environmentId: "e1234", + environmentType: "DEVELOPMENT", + queue: "task/my-task", + timestamp: Date.now(), + attempt: 0, + ...overrides, + }; +} + +const QUEUE = "task/my-task"; + +vi.setConfig({ testTimeout: 60_000 }); + +// A concurrency key is an unrestricted client string, so `*` is reachable from the public +// API, and `queueKey` renders it as `...:queue::ck:*`, which is byte-identical to the +// wildcard member the CK scripts keep in the master queue. Each of those scripts rebalances +// the master queue with that wildcard member and then removes the "old-format" entry for the +// variant it just touched. When the variant IS the wildcard, the second call undid the +// first, taking the whole base queue's master-queue entry with it: nothing pointed at the +// queue any more, so every concurrency key on it stopped being dequeued, silently, until +// some later write happened to re-add the member. +describe("concurrency key of '*'", () => { + redisTest("enqueueing it leaves the base queue reachable", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const t0 = Date.now() - 100_000; + const shard = testOptions.keys.masterQueueShardForEnvironment(authenticatedEnvDev.id, 2); + const masterQueueKey = testOptions.keys.masterQueueKeyForShard(shard); + + // An ordinary key with real queued work: the bystander that used to be taken down. + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: "r-victim", concurrencyKey: "user-1", timestamp: t0 }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + expect(await queue.redis.zcard(masterQueueKey)).toBe(1); + + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: "r-star", concurrencyKey: "*", timestamp: t0 + 1 }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + + // The master queue still points at this base queue. + expect(await queue.redis.zcard(masterQueueKey)).toBe(1); + + // Both variants are registered, and both runs come back out. + const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( + testOptions.keys.queueKey(authenticatedEnvDev, QUEUE, "user-1") + ); + expect((await queue.redis.zrange(ckIndexKey, 0, -1)).length).toBe(2); + + const served = await queue.testDequeueFromMasterQueue(shard, authenticatedEnvDev.id, 10); + expect(served.map((m) => m.messageId).sort()).toEqual(["r-star", "r-victim"]); + } finally { + await queue.quit(); + } + }); + + redisTest("acking it leaves the base queue reachable", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const t0 = Date.now() - 100_000; + const shard = testOptions.keys.masterQueueShardForEnvironment(authenticatedEnvDev.id, 2); + const masterQueueKey = testOptions.keys.masterQueueKeyForShard(shard); + + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: "r-star", concurrencyKey: "*", timestamp: t0 }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: "r-victim", concurrencyKey: "user-1", timestamp: t0 + 1 }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + + // Ack the '*' run while the other key still has work queued: the ack script runs the + // same rebalance-then-cleanup pair as the enqueue one. + await queue.acknowledgeMessage(authenticatedEnvDev.organization.id, "r-star", { + skipDequeueProcessing: true, + }); + + expect(await queue.redis.zcard(masterQueueKey)).toBe(1); + + const served = await queue.testDequeueFromMasterQueue(shard, authenticatedEnvDev.id, 10); + expect(served.map((m) => m.messageId)).toEqual(["r-victim"]); + } finally { + await queue.quit(); + } + }); + + redisTest("nacking it leaves the base queue reachable", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const t0 = Date.now() - 100_000; + const shard = testOptions.keys.masterQueueShardForEnvironment(authenticatedEnvDev.id, 2); + const masterQueueKey = testOptions.keys.masterQueueKeyForShard(shard); + + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: "r-star", concurrencyKey: "*", timestamp: t0 }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + + const [dequeued] = await queue.testDequeueFromMasterQueue(shard, authenticatedEnvDev.id, 1); + expect(dequeued?.messageId).toBe("r-star"); + + await queue.nackMessage({ + orgId: authenticatedEnvDev.organization.id, + messageId: "r-star", + retryAt: Date.now() - 1, + skipDequeueProcessing: true, + }); + + expect(await queue.redis.zcard(masterQueueKey)).toBe(1); + + const served = await queue.testDequeueFromMasterQueue(shard, authenticatedEnvDev.id, 1); + expect(served.map((m) => m.messageId)).toEqual(["r-star"]); + } finally { + await queue.quit(); + } + }); +}); From 40c4064f964d5b91b91f9bb7e00623917de53d22 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 18 Aug 2026 10:54:07 +0100 Subject: [PATCH 03/98] fix(webapp): show errors on AI tool call and embed spans in the run inspector (#4653) ## Summary When an AI SDK tool call failed inside a run, the span showed up under the "Errors only" filter but the span inspector gave no hint of what went wrong. The exception was recorded on the span all along; the `ai.toolCall` and `ai.embed` inspector views just never rendered span events. Failed tool call and embedding spans now show the standard error block (message plus stack trace) below the Input section. ## Root cause Generic spans render exception span events via the `SpanEvents` component, but the AI-specific span entities replace the whole panel with their own layout and dropped the events entirely. The span's events are now passed into `AIToolCallSpanDetails` and `AIEmbedSpanDetails` and rendered with the same `SpanEvents` component the generic view uses. Errored generation spans (`ai.generateText` and friends) use a tabbed view and still don't surface errors; that needs its own design pass and is left for a follow-up. --- .server-changes/ai-tool-call-span-errors.md | 6 ++++++ .../components/runs/v3/ai/AIEmbedSpanDetails.tsx | 16 +++++++++++++++- .../runs/v3/ai/AIToolCallSpanDetails.tsx | 16 +++++++++++++++- .../route.tsx | 4 ++-- 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 .server-changes/ai-tool-call-span-errors.md diff --git a/.server-changes/ai-tool-call-span-errors.md b/.server-changes/ai-tool-call-span-errors.md new file mode 100644 index 00000000000..31ab5ba5ddd --- /dev/null +++ b/.server-changes/ai-tool-call-span-errors.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Failed AI SDK tool call and embedding spans now show the error message and stack trace in the run inspector, below the tool input. diff --git a/apps/webapp/app/components/runs/v3/ai/AIEmbedSpanDetails.tsx b/apps/webapp/app/components/runs/v3/ai/AIEmbedSpanDetails.tsx index 1cac3de503f..25ed6c8a29a 100644 --- a/apps/webapp/app/components/runs/v3/ai/AIEmbedSpanDetails.tsx +++ b/apps/webapp/app/components/runs/v3/ai/AIEmbedSpanDetails.tsx @@ -1,5 +1,7 @@ +import type { SpanEvent as OtelSpanEvent } from "@trigger.dev/core/v3"; import { Header3 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; +import { SpanEvents } from "~/components/runs/v3/SpanEvents"; import { formatDuration } from "./aiHelpers"; import { SpanMetricRow as MetricRow } from "./SpanMetricRow"; @@ -35,7 +37,13 @@ export function extractAIEmbedData( }; } -export function AIEmbedSpanDetails({ data }: { data: AIEmbedData }) { +export function AIEmbedSpanDetails({ + data, + spanEvents, +}: { + data: AIEmbedData; + spanEvents?: OtelSpanEvent[]; +}) { return (
@@ -58,6 +66,12 @@ export function AIEmbedSpanDetails({ data }: { data: AIEmbedData }) {
)} + + {spanEvents && spanEvents.some((event) => !event.name.startsWith("trigger.dev/")) && ( +
+ +
+ )} diff --git a/apps/webapp/app/components/runs/v3/ai/AIToolCallSpanDetails.tsx b/apps/webapp/app/components/runs/v3/ai/AIToolCallSpanDetails.tsx index 5557f2728c2..e35c9a7f698 100644 --- a/apps/webapp/app/components/runs/v3/ai/AIToolCallSpanDetails.tsx +++ b/apps/webapp/app/components/runs/v3/ai/AIToolCallSpanDetails.tsx @@ -1,6 +1,8 @@ +import type { SpanEvent as OtelSpanEvent } from "@trigger.dev/core/v3"; import { Header3 } from "~/components/primitives/Headers"; import { CodeBlock } from "~/components/code/CodeBlock"; import { TruncatedCopyableValue } from "~/components/primitives/TruncatedCopyableValue"; +import { SpanEvents } from "~/components/runs/v3/SpanEvents"; import { formatDuration, tryPrettyJson } from "./aiHelpers"; import { SpanMetricRow as MetricRow } from "./SpanMetricRow"; @@ -36,7 +38,13 @@ export function extractAIToolCallData( }; } -export function AIToolCallSpanDetails({ data }: { data: AIToolCallData }) { +export function AIToolCallSpanDetails({ + data, + spanEvents, +}: { + data: AIToolCallData; + spanEvents?: OtelSpanEvent[]; +}) { return (
@@ -68,6 +76,12 @@ export function AIToolCallSpanDetails({ data }: { data: AIToolCallData }) { />
)} + + {spanEvents && spanEvents.some((event) => !event.name.startsWith("trigger.dev/")) && ( +
+ +
+ )}
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx index 374440dfa0b..807e527793c 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx @@ -1777,7 +1777,7 @@ function SpanEntity({ span }: { span: Span }) {
- + ); } @@ -1787,7 +1787,7 @@ function SpanEntity({ span }: { span: Span }) {
- + ); } From b33197691b4936192f3d7bb741fd91735f7963ec Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 18 Aug 2026 11:35:51 +0100 Subject: [PATCH 04/98] chore: enforce no unused deps or code in ci (#4654) --- .github/workflows/code-quality.yml | 3 + AGENTS.md | 12 + CONTRIBUTING.md | 7 +- .../src/backpressure/backpressureMonitor.ts | 2 +- apps/supervisor/src/clients/kubernetes.ts | 2 +- apps/supervisor/src/wideEvents/index.ts | 10 +- apps/supervisor/src/wideEvents/state.ts | 2 +- apps/supervisor/src/workloadToken.ts | 1 - apps/webapp/app/assets/logos/ATAndTLogo.tsx | 21 - apps/webapp/app/assets/logos/AstroLogo.tsx | 57 - apps/webapp/app/assets/logos/ExpressLogo.tsx | 15 - apps/webapp/app/assets/logos/FastifyLogo.tsx | 45 - apps/webapp/app/assets/logos/NestjsLogo.tsx | 16 - apps/webapp/app/assets/logos/NextjsLogo.tsx | 47 - apps/webapp/app/assets/logos/NuxtLogo.tsx | 21 - apps/webapp/app/assets/logos/RedwoodLogo.tsx | 42 - apps/webapp/app/assets/logos/RemixLogo.tsx | 199 -- apps/webapp/app/assets/logos/ShopifyLogo.tsx | 39 - .../webapp/app/assets/logos/SveltekitLogo.tsx | 29 - apps/webapp/app/assets/logos/VerizonLogo.tsx | 33 - .../app/components/BlankStatePanels.tsx | 71 +- apps/webapp/app/components/ErrorDisplay.tsx | 2 +- apps/webapp/app/components/FeatureBadges.tsx | 18 - .../app/components/GitHubLoginButton.tsx | 27 - apps/webapp/app/components/GitMetadata.tsx | 10 +- .../app/components/MachineLabelCombo.tsx | 2 +- .../app/components/ProductHuntBanner.tsx | 24 - apps/webapp/app/components/SetupCommands.tsx | 46 - .../backOffice/ApiRateLimitSection.server.ts | 2 +- .../BatchRateLimitSection.server.ts | 2 +- .../admin/backOffice/RateLimitSection.tsx | 4 +- apps/webapp/app/components/admin/debugRun.tsx | 2 +- .../components/billing/billingAlertsFormat.ts | 50 +- apps/webapp/app/components/code/CodeBlock.tsx | 2 +- .../app/components/code/InstallPackages.tsx | 44 - apps/webapp/app/components/code/tsql/index.ts | 10 - .../DashboardAgentSuggestedPrompts.tsx | 20 +- .../dashboard-agent/agent-identity.ts | 2 +- .../dashboard-agent/chat-layout.test.ts | 2 - .../dashboard-agent/chat-layout.tsx | 18 - .../dashboardAgentOpenRequest.ts | 4 +- .../dashboard-agent/demo/fixtures/chart.ts | 19 +- .../dashboard-agent/demo/fixtures/intents.ts | 10 +- .../demo/fixtures/investigation.ts | 16 +- .../dashboard-agent/demo/fixtures/messages.ts | 2 +- .../demo/fixtures/page-context.ts | 28 +- .../dashboard-agent/demo/fixtures/watches.ts | 12 +- .../components/dashboard-agent/demo/ids.ts | 8 +- .../components/dashboard-agent/demo/index.ts | 2 +- .../dashboard-agent/page-context-types.ts | 6 +- .../dashboard-agent/panel-layout.tsx | 2 +- .../dashboard-agent/progress-line.ts | 4 +- .../dashboard-agent/report-sparkline.tsx | 23 +- .../app/components/dashboard-agent/run-id.ts | 2 +- .../dashboard-agent/settled-transcript.ts | 2 +- .../suggested-prompts/dismissal.ts | 11 - .../suggested-prompts/index.ts | 30 +- .../suggested-prompts/page-prompts.ts | 2 +- .../suggested-prompts/registry.ts | 14 +- .../suggested-prompts/signal-prompts.ts | 23 +- .../tooltip-accessible-name.test.ts | 1 - .../components/dashboard-agent/wake-poll.ts | 2 +- .../components/dashboard-agent/watch-chips.ts | 2 +- .../dashboard-agent/watch-recommendations.ts | 2 +- .../app/components/layout/MetricsLayout.tsx | 14 +- .../app/components/metrics/QueryWidget.tsx | 2 +- .../navigation/CustomizeSidebarDialog.tsx | 2 +- .../navigation/EnvironmentSelector.tsx | 2 +- .../components/navigation/sideMenuTypes.ts | 7 - .../onboarding/TechnologyPicker.tsx | 2 +- .../components/primitives/AgentDotMatrix.tsx | 4 +- .../app/components/primitives/Alert.tsx | 1 - .../components/primitives/AnimatingArrow.tsx | 177 -- .../app/components/primitives/Avatar.tsx | 4 +- .../app/components/primitives/Buttons.tsx | 20 +- .../app/components/primitives/ClientTabs.tsx | 11 - .../app/components/primitives/DateTime.tsx | 50 +- .../app/components/primitives/Dialog.tsx | 2 - .../app/components/primitives/FormError.tsx | 23 - .../app/components/primitives/Headers.tsx | 2 - .../app/components/primitives/Input.tsx | 2 +- .../app/components/primitives/InputOTP.tsx | 11 +- .../components/primitives/LabelValueStack.tsx | 93 - .../primitives/LoadingBarDivider.tsx | 2 +- .../app/components/primitives/Popover.tsx | 47 - .../components/primitives/PrettyDuration.tsx | 40 - .../app/components/primitives/Select.tsx | 14 +- .../app/components/primitives/Sheet.tsx | 201 -- .../app/components/primitives/SheetV3.tsx | 15 +- .../app/components/primitives/Table.tsx | 26 - .../webapp/app/components/primitives/Tabs.tsx | 2 +- .../app/components/primitives/Timeline.tsx | 4 +- .../app/components/primitives/Tooltip.tsx | 2 +- .../primitives/TreeView/TreeView.tsx | 3 - .../components/primitives/TreeView/utils.ts | 2 +- .../components/primitives/charts/Chart.tsx | 12 +- .../primitives/charts/ChartCompound.tsx | 10 - .../primitives/charts/DateRangeContext.tsx | 8 +- .../charts/hooks/useHighlightState.ts | 33 +- .../charts/hooks/useZoomSelection.ts | 4 +- .../primitives/charts/statusColors.ts | 4 +- .../app/components/primitives/useTableSort.ts | 58 - .../app/components/query/QueryEditor.tsx | 2 +- .../components/queues/QueueMetricCards.tsx | 118 +- .../webapp/app/components/run/RunTimeline.tsx | 6 +- .../app/components/runs/v3/BatchFilters.tsx | 2 +- .../app/components/runs/v3/BatchStatus.tsx | 12 +- .../app/components/runs/v3/BulkAction.tsx | 24 +- .../runs/v3/CheckBatchCompletionDialog.tsx | 58 - .../components/runs/v3/DeploymentStatus.tsx | 8 +- .../app/components/runs/v3/LiveTimer.tsx | 31 - .../app/components/runs/v3/RunFilters.tsx | 4 +- .../components/runs/v3/ScheduleFilters.tsx | 248 --- .../app/components/runs/v3/SharedFilters.tsx | 2 +- .../app/components/runs/v3/SpanEvents.tsx | 2 +- .../app/components/runs/v3/SpanTitle.tsx | 2 +- .../app/components/runs/v3/TaskPath.tsx | 18 - .../runs/v3/TaskRunAttemptStatus.tsx | 17 +- .../app/components/runs/v3/TaskRunStatus.tsx | 40 +- .../components/runs/v3/WaitpointStatus.tsx | 6 +- .../webapp/app/components/runs/v3/ai/index.ts | 3 - .../webapp/app/components/runs/v3/ai/types.ts | 8 +- .../schedules/ScheduleInspector.tsx | 2 +- .../components/sessions/v1/SessionFilters.tsx | 3 +- .../components/sessions/v1/SessionStatus.tsx | 6 +- .../webhookConsole/WebhookComposer.tsx | 2 +- .../webhookDeliveries/v1/DeliveryStatus.tsx | 4 +- .../v1/buildDeliveryTimelineItems.ts | 4 +- apps/webapp/app/consts.ts | 16 +- apps/webapp/app/database-types.ts | 8 - apps/webapp/app/db.server.ts | 11 +- apps/webapp/app/hooks/useCanViewLogsPage.ts | 16 - apps/webapp/app/hooks/useEnvironments.ts | 12 - apps/webapp/app/hooks/useList.tsx | 2 +- apps/webapp/app/hooks/useOrganizations.ts | 12 +- apps/webapp/app/hooks/useRevalidateOnParam.ts | 57 - apps/webapp/app/hooks/useTextFilter.ts | 26 - apps/webapp/app/hooks/useThrottle.ts | 23 - apps/webapp/app/hooks/useToggleFilter.ts | 21 - apps/webapp/app/hooks/useTypedMatchData.ts | 2 +- apps/webapp/app/models/member.server.ts | 2 +- apps/webapp/app/models/message.server.ts | 32 - apps/webapp/app/models/projectAlert.server.ts | 4 - .../app/models/runtimeEnvironment.server.ts | 71 - apps/webapp/app/models/task.server.ts | 1 - apps/webapp/app/models/taskQueue.server.ts | 59 +- apps/webapp/app/models/user.server.ts | 25 +- .../app/models/vercelIntegration.server.ts | 4 +- .../app/models/vercelSdkRecovery.server.ts | 2 +- .../app/presenters/ProjectPresenter.server.ts | 78 - .../v3/AgentDetailPresenter.server.ts | 2 +- .../v3/AgentListPresenter.server.ts | 16 +- .../v3/AlertChannelListPresenter.server.ts | 5 +- .../v3/ApiAlertChannelPresenter.server.ts | 8 +- .../v3/ApiErrorGroupPresenter.server.ts | 2 +- .../v3/ApiWebhookDeliveryPresenter.server.ts | 2 +- .../v3/ApiWebhookEndpointPresenter.server.ts | 2 +- .../v3/BatchListPresenter.server.ts | 2 - .../presenters/v3/BatchPresenter.server.ts | 2 - .../presenters/v3/BranchesPresenter.server.ts | 3 - .../v3/DeploymentListPresenter.server.ts | 2 +- .../EnvironmentVariablesPresenter.server.ts | 2 +- .../v3/ErrorGroupPresenter.server.ts | 18 +- .../v3/ErrorsListPresenter.server.ts | 18 - .../v3/LogDetailPresenter.server.ts | 2 - .../presenters/v3/LogsListPresenter.server.ts | 3 +- .../v3/MetricDashboardPresenter.server.ts | 16 - .../v3/ModelRegistryPresenter.server.ts | 4 +- .../v3/QueueListPresenter.server.ts | 2 +- .../app/presenters/v3/RunPresenter.server.ts | 4 - .../v3/RunTagListPresenter.server.ts | 3 - .../v3/ScheduleListPresenter.server.ts | 4 +- .../v3/SessionListPresenter.server.ts | 1 - .../presenters/v3/SessionPresenter.server.ts | 2 - .../v3/TaskDetailPresenter.server.ts | 6 +- .../presenters/v3/TaskListPresenter.server.ts | 2 +- .../app/presenters/v3/TaskPresenter.server.ts | 83 - .../v3/TasksDashboardPresenter.server.ts | 6 +- .../app/presenters/v3/TestPresenter.server.ts | 2 +- .../v3/UnifiedTaskListPresenter.server.ts | 4 +- .../presenters/v3/UsagePresenter.server.ts | 10 - .../v3/VercelSettingsPresenter.server.ts | 2 +- .../v3/WaitpointTagListPresenter.server.ts | 3 - .../v3/WebhookDetailPresenter.server.ts | 4 +- .../v3/dashboardAgent/watch-wording.ts | 13 - .../v3/queueListPagination.server.ts | 4 +- .../presenters/v3/reports/health/execution.ts | 2 +- .../app/presenters/v3/reports/health/flow.ts | 2 +- .../v3/reports/health/health-data.ts | 10 +- .../presenters/v3/reports/health/health.ts | 4 +- .../presenters/v3/reports/report-layout.ts | 31 +- .../v3/reports/report-view-model.ts | 7 - .../concerns/computeMigration.server.ts | 2 +- .../app/runEngine/concerns/queues.server.ts | 4 +- .../services/streamBatchItems.server.ts | 2 +- apps/webapp/app/runEngine/types.ts | 8 +- apps/webapp/app/services/apiAuth.server.ts | 105 +- apps/webapp/app/services/attio.server.ts | 2 +- .../services/authFeatureControls.server.ts | 3 - .../app/services/authTelemetry.server.ts | 2 +- ...authorizationRateLimitMiddleware.server.ts | 10 +- .../services/autoIncrementCounter.server.ts | 86 - .../betterstack/betterstack.server.ts | 2 +- .../app/services/billingLimit.schemas.ts | 24 +- .../clickhouse/clickhouseFactory.server.ts | 8 - .../clickhouseSecretSchemas.server.ts | 6 - ...hboardAgentAlertUnsubscribeToken.server.ts | 4 +- .../services/dashboardAgentBodyCap.server.ts | 2 +- .../dashboardAgentWatchAlerts.server.ts | 2 +- .../app/services/dashboardAgentWatchChecks.ts | 19 +- .../dashboardAgentWatchInvestigate.server.ts | 2 +- .../services/dashboardAgentWatchRunChecks.ts | 6 +- .../dashboardAgentWatchSweep.server.ts | 6 +- .../dashboardAgentWatchToken.server.ts | 6 +- .../services/dashboardAgentWatches.server.ts | 17 +- .../services/dashboardPreferences.server.ts | 17 +- ...ganizationDataStoreConfigSchemas.server.ts | 6 +- apps/webapp/app/services/email.server.ts | 6 +- .../environmentMetricsRepository.server.ts | 2 +- .../environmentVariableApiAccess.server.ts | 2 +- .../app/services/impersonation.server.ts | 4 +- .../app/services/lastAuthMethod.server.ts | 2 +- apps/webapp/app/services/logger.server.ts | 26 - .../mfa/mfaRateLimiterGlobal.server.ts | 3 - .../app/services/onboardingSession.server.ts | 49 - .../organizationAccessToken.server.ts | 84 - .../services/platformNotifications.server.ts | 5 +- .../preferences/uiPreferences.server.ts | 2 +- apps/webapp/app/services/promoCode.server.ts | 2 +- .../app/services/publicTokens.server.ts | 2 +- .../app/services/queryService.server.ts | 6 +- .../webapp/app/services/rateLimiter.server.ts | 8 +- .../realtime/electricStreamProtocol.server.ts | 2 +- .../realtime/envChangeRouter.server.ts | 6 +- .../app/services/realtime/jwtAuth.server.ts | 4 +- .../services/realtime/mintRunToken.server.ts | 41 - .../realtime/nativeRealtimeClient.server.ts | 4 +- .../realtime/replicaLagEstimator.server.ts | 2 +- .../realtime/runChangeNotifier.server.ts | 2 +- .../runChangeNotifierInstance.server.ts | 18 - .../app/services/realtime/runReader.server.ts | 2 +- .../realtime/s2realtimeStreams.server.ts | 6 +- .../realtime/sessionRunManager.server.ts | 6 +- .../services/realtime/shadowCompare.server.ts | 2 +- .../realtime/streamBasinProvisioner.server.ts | 10 +- .../app/services/realtime/utils.server.ts | 33 - .../realtime/v1StreamsGlobal.server.ts | 9 +- apps/webapp/app/services/redirectTo.server.ts | 7 +- .../app/services/referralSource.server.ts | 6 +- .../app/services/renderMarkdown.server.ts | 21 - .../services/runsReplicationGlobal.server.ts | 12 - .../runsRepository/runsRepository.server.ts | 2 +- .../secretStoreOptionsSchema.server.ts | 2 +- .../app/services/sensitiveDataReplacer.ts | 4 +- apps/webapp/app/services/session.server.ts | 2 +- .../app/services/sessionStorage.server.ts | 2 +- .../sessionsRepository.server.ts | 12 +- apps/webapp/app/services/signals.server.ts | 6 +- apps/webapp/app/services/slack.server.ts | 43 - apps/webapp/app/services/ssoAuth.server.ts | 2 +- .../services/taskIdentifierCache.server.ts | 14 - .../services/userActorEnvironment.server.ts | 2 +- .../webhookDeliveriesRepository.server.ts | 2 +- apps/webapp/app/utils.ts | 76 - apps/webapp/app/utils/apiCors.ts | 7 - apps/webapp/app/utils/cspImageOrigins.ts | 2 +- .../app/utils/databaseMetrics.server.ts | 10 +- apps/webapp/app/utils/delays.ts | 14 - apps/webapp/app/utils/inviteRoleLadder.ts | 2 +- apps/webapp/app/utils/json.ts | 55 - apps/webapp/app/utils/lerp.ts | 2 +- apps/webapp/app/utils/logUtils.ts | 2 - apps/webapp/app/utils/modelFormatters.ts | 3 - apps/webapp/app/utils/objects.ts | 14 - apps/webapp/app/utils/pageSwitching.ts | 2 +- apps/webapp/app/utils/pageTitle.ts | 6 +- .../app/utils/parseRequestJson.server.ts | 29 - apps/webapp/app/utils/pathBuilder.ts | 42 +- apps/webapp/app/utils/permissionDenied.ts | 2 +- apps/webapp/app/utils/plainCustomerCards.ts | 2 - .../utils/queryPerformanceMonitor.server.ts | 4 +- apps/webapp/app/utils/redactor.ts | 71 - apps/webapp/app/utils/semver.ts | 2 +- apps/webapp/app/utils/sse.ts | 10 +- apps/webapp/app/utils/tablerIcons.ts | 2 - apps/webapp/app/utils/taskListToTree.ts | 30 - apps/webapp/app/utils/themePreference.ts | 2 +- apps/webapp/app/utils/timelineSpanEvents.ts | 6 +- .../app/utils/webhookIngressUrl.server.ts | 2 +- .../app/v3/billingLimitWorker.server.ts | 2 +- apps/webapp/app/v3/canAccessAi.server.ts | 47 - apps/webapp/app/v3/electricShape.server.ts | 2 +- .../webapp/app/v3/engineDeprecation.server.ts | 2 +- .../environmentVariablesRepository.server.ts | 15 +- .../app/v3/environmentVariables/repository.ts | 2 +- .../clickhouseEventRepository.server.ts | 46 - .../eventRepository/eventRepository.types.ts | 20 +- .../app/v3/eventRepository/index.server.ts | 32 - .../sanitizeRowsOnParseError.server.ts | 8 +- .../v3/eventRepository/traceExport.server.ts | 2 +- apps/webapp/app/v3/featureFlags.server.ts | 5 +- apps/webapp/app/v3/featureFlags.ts | 7 +- .../app/v3/models/workerDeployment.server.ts | 105 +- .../v3/mollifier/idempotencyClaim.server.ts | 6 +- .../app/v3/mollifier/mollifierGate.server.ts | 2 +- .../v3/mollifier/mollifierMollify.server.ts | 2 +- .../v3/mollifier/mollifierTelemetry.server.ts | 34 +- .../mollifierTripEvaluator.server.ts | 2 +- .../v3/mollifier/mutateWithFallback.server.ts | 8 +- apps/webapp/app/v3/querySchemas.ts | 10 +- apps/webapp/app/v3/queueDepthSeries.ts | 4 +- .../controlPlaneCache.server.ts | 6 +- .../v3/runOpsMigration/readThrough.server.ts | 4 +- .../v3/runOpsMigration/splitMode.server.ts | 2 +- apps/webapp/app/v3/scheduleEngine.server.ts | 2 - .../app/v3/services/aiQueryService.server.ts | 11 - .../v3/services/aiTitleRateLimiter.server.ts | 2 +- .../alerts/errorGroupWebhook.server.ts | 2 +- .../alerts/safeWebhookFetch.server.ts | 2 +- .../billingLimit/billingLimitConstants.ts | 2 - .../BulkActionV2.batchReadThrough.server.ts | 2 +- .../v3/services/bulk/BulkActionV2.server.ts | 2 +- .../v3/services/concurrencySystem.server.ts | 4 +- .../services/createBackgroundWorker.server.ts | 1 - .../v3/services/duplicateTaskIds.server.ts | 2 +- .../app/v3/services/projectPubSub.server.ts | 3 - .../app/v3/services/tracePubSub.server.ts | 4 +- .../services/worker/sanitizeWorkerHeaders.ts | 2 +- .../worker/workerGroupTokenService.server.ts | 4 +- .../workloadTokenAuthorization.server.ts | 2 +- apps/webapp/app/v3/taskEventStore.server.ts | 4 +- apps/webapp/app/v3/taskStatus.ts | 66 +- apps/webapp/app/v3/tracer.server.ts | 46 +- apps/webapp/app/v3/tracing.server.ts | 51 - .../v3/utils/calculateNextSchedule.server.ts | 2 +- apps/webapp/app/v3/utils/maxDuration.ts | 16 - .../app/v3/vercel/vercelOAuthState.server.ts | 2 +- .../vercel/vercelProjectIntegrationSchema.ts | 38 +- apps/webapp/app/v3/webhookEngine.server.ts | 2 - apps/webapp/package.json | 18 - apps/webapp/test/otlpMetrics.helpers.ts | 2 +- apps/webapp/test/setup-test-env.ts | 4 - apps/webapp/test/utils/streams.ts | 46 - internal-packages/cache/package.json | 1 - .../clickhouse/src/client/errors.ts | 4 +- .../clickhouse/src/client/tsql.ts | 2 +- .../dashboard-agent/src/compaction.ts | 4 +- .../dashboard-agent/src/eval-policy.ts | 6 +- .../dashboard-agent/src/repo-tools.ts | 2 +- .../dashboard-agent/src/tool-api-client.ts | 2 +- .../dashboard-agent/src/tool-api.ts | 2 +- .../dashboard-agent/src/tool-evidence.ts | 2 +- .../src/tool-investigations.ts | 4 +- .../dashboard-agent/src/tools.ts | 2 - .../dashboard-agent/src/watch-delivery.ts | 2 +- .../dashboard-agent/src/watch-lifecycle.ts | 2 +- .../dashboard-agent/src/watch-tick.ts | 12 +- internal-packages/database/package.json | 1 - .../emails/emails/components/styles.ts | 26 - internal-packages/emails/package.json | 1 - .../observability-map/src/mutations.ts | 4 +- .../observability-map/src/report/prComment.ts | 2 +- .../observability-map/src/report/terminal.ts | 2 +- .../observability-map/src/score.ts | 2 +- .../observability-map/src/sensitivity.ts | 3 - .../otlp-importer/jest.config.js | 8 - internal-packages/otlp-importer/package.json | 1 - .../proto/collector/logs/v1/logs_service.ts | 12 +- .../collector/metrics/v1/metrics_service.ts | 12 +- .../proto/collector/trace/v1/trace_service.ts | 12 +- .../opentelemetry/proto/common/v1/common.ts | 10 +- .../opentelemetry/proto/logs/v1/logs.ts | 20 +- .../opentelemetry/proto/metrics/v1/metrics.ts | 32 +- .../proto/resource/v1/resource.ts | 6 +- .../opentelemetry/proto/trace/v1/trace.ts | 22 +- .../otlp-importer/tsup.config.ts | 19 - .../src/engine/controlPlaneResolver.ts | 8 +- .../src/engine/systems/debounceSystem.ts | 6 - .../src/engine/systems/runAttemptSystem.ts | 2 +- .../tests/helpers/executionStateMachine.ts | 257 --- .../tests/helpers/snapshotTestHelpers.ts | 6 +- .../src/engine/tests/utils/engineTest.ts | 134 -- .../run-engine/src/engine/ttlWorkerCatalog.ts | 2 - .../run-engine/src/engine/types.ts | 2 +- .../run-engine/src/run-queue/constants.ts | 3 - .../run-engine/src/run-queue/errors.ts | 5 - .../run-queue/fairQueueSelectionStrategy.ts | 11 +- .../run-engine/src/run-queue/index.ts | 2 +- internal-packages/run-store/package.json | 2 +- internal-packages/schedule-engine/README.md | 8 +- .../schedule-engine/package.json | 1 - .../src/engine/scheduleCalculation.ts | 19 - .../schedule-engine/src/engine/types.ts | 15 +- .../sdk-compat-tests/package.json | 4 +- internal-packages/sso/package.json | 1 - internal-packages/testcontainers/package.json | 1 - .../testcontainers/src/docker.ts | 4 +- internal-packages/testcontainers/src/utils.ts | 13 - internal-packages/tsql/package.json | 4 +- internal-packages/tsql/src/query/constants.ts | 39 +- internal-packages/tsql/src/query/context.ts | 9 +- internal-packages/tsql/src/query/database.ts | 72 +- internal-packages/tsql/src/query/escape.ts | 2 +- internal-packages/tsql/src/query/models.ts | 31 - .../tsql/src/query/parse_string.ts | 20 - .../tsql/src/query/property_types.ts | 194 -- internal-packages/tsql/src/query/schema.ts | 23 - .../webhook-engine/src/engine/filter/index.ts | 7 +- .../webhook-engine/src/engine/partitions.ts | 8 +- .../src/engine/verification/parse.ts | 2 +- knip.json | 92 +- lefthook.yml | 12 + package.json | 8 +- packages/build/package.json | 3 - packages/build/src/version.ts | 1 - packages/cli-v3/package.json | 19 - packages/cli-v3/src/build/buildWorker.ts | 3 +- packages/cli-v3/src/build/externals.ts | 4 +- packages/cli-v3/src/build/packageModules.ts | 32 +- packages/cli-v3/src/build/plugins.ts | 4 +- packages/cli-v3/src/commands/analyze.ts | 4 +- packages/cli-v3/src/commands/deploy.ts | 4 +- packages/cli-v3/src/commands/dev.ts | 2 +- packages/cli-v3/src/commands/init.ts | 6 +- packages/cli-v3/src/commands/install-mcp.ts | 2 +- packages/cli-v3/src/commands/list-profiles.ts | 4 +- packages/cli-v3/src/commands/login.ts | 6 +- packages/cli-v3/src/commands/logout.ts | 4 +- packages/cli-v3/src/commands/mcp.ts | 2 +- packages/cli-v3/src/commands/mint-token.ts | 2 +- packages/cli-v3/src/commands/preview.ts | 2 +- packages/cli-v3/src/commands/promote.ts | 2 +- packages/cli-v3/src/commands/skills.ts | 2 +- packages/cli-v3/src/commands/switch.ts | 4 +- packages/cli-v3/src/commands/trigger.ts | 121 -- packages/cli-v3/src/commands/update.ts | 2 +- packages/cli-v3/src/commands/whoami.ts | 2 +- packages/cli-v3/src/commands/workers/build.ts | 603 ------ .../cli-v3/src/commands/workers/create.ts | 135 -- packages/cli-v3/src/commands/workers/index.ts | 16 - packages/cli-v3/src/commands/workers/list.ts | 119 -- packages/cli-v3/src/commands/workers/run.ts | 151 -- packages/cli-v3/src/consts.ts | 1 - packages/cli-v3/src/deploy/buildImage.ts | 2 +- packages/cli-v3/src/deploy/logs.ts | 2 +- packages/cli-v3/src/dev/workerRuntime.ts | 12 +- packages/cli-v3/src/mcp/auth.ts | 25 +- packages/cli-v3/src/mcp/schemas.ts | 6 +- packages/cli-v3/src/rules/install.ts | 0 packages/cli-v3/src/types.ts | 6 - packages/cli-v3/src/utilities/analyze.ts | 2 +- packages/cli-v3/src/utilities/cliOutput.ts | 6 +- packages/cli-v3/src/utilities/configFiles.ts | 2 +- .../src/utilities/createFileFromTemplate.ts | 2 +- packages/cli-v3/src/utilities/fileSystem.ts | 28 +- .../cli-v3/src/utilities/getApiKeyType.ts | 65 - packages/cli-v3/src/utilities/keyValueBy.ts | 39 - packages/cli-v3/src/utilities/logger.ts | 24 +- .../cli-v3/src/utilities/obfuscateApiKey.ts | 4 - .../cli-v3/src/utilities/parseNameAndPath.ts | 11 - .../src/utilities/resolveInternalFilePath.ts | 8 - .../cli-v3/src/utilities/safeJsonParse.ts | 11 - packages/cli-v3/src/utilities/sourceFiles.ts | 2 +- .../src/utilities/supportsHyperlinks.ts | 2 +- packages/cli-v3/src/utilities/taskFiles.ts | 103 - packages/cli-v3/src/utilities/windows.ts | 6 +- packages/core/package.json | 6 - packages/core/src/debounce.ts | 20 - packages/core/src/v3/apiClient/runStream.ts | 6 +- packages/core/src/v3/apiClient/stream.ts | 34 - .../core/src/v3/apiClientManager/index.ts | 2 +- .../core/src/v3/clock/preciseWallClock.ts | 2 +- packages/core/src/v3/lifecycleHooks/types.ts | 6 +- packages/core/src/v3/logger/taskLogger.ts | 2 +- packages/core/src/v3/otel/tracingSDK.ts | 2 +- packages/core/src/v3/realtimeStreams/index.ts | 12 - .../v3/runEngineWorker/supervisor/events.ts | 2 - .../src/v3/runEngineWorker/supervisor/util.ts | 26 - .../core/src/v3/test/mock-task-context.ts | 2 +- packages/core/src/v3/types/schemas.ts | 22 +- packages/core/src/v3/usage/usageClient.ts | 5 - .../src/v3/utils/safeAsyncLocalStorage.ts | 21 - packages/plugins/src/rbac.ts | 2 +- packages/python/package.json | 1 - .../src/utils/createContextAndHook.ts | 2 +- packages/react-hooks/src/utils/trigger-swr.ts | 2 +- packages/redis-worker/package.json | 2 - .../src/fair-queue/schedulers/roundRobin.ts | 2 +- packages/rsc/package.json | 3 - packages/trigger-sdk/package.json | 14 +- packages/trigger-sdk/src/v3/auth.ts | 6 +- packages/trigger-sdk/src/v3/chat-client.ts | 8 +- packages/trigger-sdk/src/v3/retry.ts | 2 +- packages/trigger-sdk/src/v3/runs.ts | 7 +- packages/trigger-sdk/src/v3/shared.ts | 10 - .../src/v3/test/mock-chat-agent.ts | 4 +- .../src/v3/test/test-session-handle.ts | 2 +- pnpm-lock.yaml | 1668 +---------------- 498 files changed, 813 insertions(+), 9491 deletions(-) delete mode 100644 apps/webapp/app/assets/logos/ATAndTLogo.tsx delete mode 100644 apps/webapp/app/assets/logos/AstroLogo.tsx delete mode 100644 apps/webapp/app/assets/logos/ExpressLogo.tsx delete mode 100644 apps/webapp/app/assets/logos/FastifyLogo.tsx delete mode 100644 apps/webapp/app/assets/logos/NestjsLogo.tsx delete mode 100644 apps/webapp/app/assets/logos/NextjsLogo.tsx delete mode 100644 apps/webapp/app/assets/logos/NuxtLogo.tsx delete mode 100644 apps/webapp/app/assets/logos/RedwoodLogo.tsx delete mode 100644 apps/webapp/app/assets/logos/RemixLogo.tsx delete mode 100644 apps/webapp/app/assets/logos/ShopifyLogo.tsx delete mode 100644 apps/webapp/app/assets/logos/SveltekitLogo.tsx delete mode 100644 apps/webapp/app/assets/logos/VerizonLogo.tsx delete mode 100644 apps/webapp/app/components/ProductHuntBanner.tsx delete mode 100644 apps/webapp/app/components/code/InstallPackages.tsx delete mode 100644 apps/webapp/app/components/code/tsql/index.ts delete mode 100644 apps/webapp/app/components/primitives/AnimatingArrow.tsx delete mode 100644 apps/webapp/app/components/primitives/LabelValueStack.tsx delete mode 100644 apps/webapp/app/components/primitives/PrettyDuration.tsx delete mode 100644 apps/webapp/app/components/primitives/Sheet.tsx delete mode 100644 apps/webapp/app/components/runs/v3/CheckBatchCompletionDialog.tsx delete mode 100644 apps/webapp/app/hooks/useCanViewLogsPage.ts delete mode 100644 apps/webapp/app/hooks/useEnvironments.ts delete mode 100644 apps/webapp/app/hooks/useRevalidateOnParam.ts delete mode 100644 apps/webapp/app/hooks/useTextFilter.ts delete mode 100644 apps/webapp/app/hooks/useThrottle.ts delete mode 100644 apps/webapp/app/hooks/useToggleFilter.ts delete mode 100644 apps/webapp/app/presenters/ProjectPresenter.server.ts delete mode 100644 apps/webapp/app/presenters/v3/TaskPresenter.server.ts delete mode 100644 apps/webapp/app/services/autoIncrementCounter.server.ts delete mode 100644 apps/webapp/app/services/onboardingSession.server.ts delete mode 100644 apps/webapp/app/services/realtime/mintRunToken.server.ts delete mode 100644 apps/webapp/app/services/realtime/utils.server.ts delete mode 100644 apps/webapp/app/services/renderMarkdown.server.ts delete mode 100644 apps/webapp/app/services/slack.server.ts delete mode 100644 apps/webapp/app/utils/objects.ts delete mode 100644 apps/webapp/app/utils/parseRequestJson.server.ts delete mode 100644 apps/webapp/app/utils/redactor.ts delete mode 100644 apps/webapp/app/utils/taskListToTree.ts delete mode 100644 apps/webapp/app/v3/canAccessAi.server.ts delete mode 100644 apps/webapp/test/setup-test-env.ts delete mode 100644 apps/webapp/test/utils/streams.ts delete mode 100644 internal-packages/otlp-importer/jest.config.js delete mode 100644 internal-packages/otlp-importer/tsup.config.ts delete mode 100644 internal-packages/run-engine/src/engine/tests/helpers/executionStateMachine.ts delete mode 100644 internal-packages/run-engine/src/engine/tests/utils/engineTest.ts delete mode 100644 internal-packages/run-engine/src/run-queue/errors.ts delete mode 100644 packages/build/src/version.ts delete mode 100644 packages/cli-v3/src/commands/trigger.ts delete mode 100644 packages/cli-v3/src/commands/workers/build.ts delete mode 100644 packages/cli-v3/src/commands/workers/create.ts delete mode 100644 packages/cli-v3/src/commands/workers/index.ts delete mode 100644 packages/cli-v3/src/commands/workers/list.ts delete mode 100644 packages/cli-v3/src/commands/workers/run.ts delete mode 100644 packages/cli-v3/src/rules/install.ts delete mode 100644 packages/cli-v3/src/types.ts delete mode 100644 packages/cli-v3/src/utilities/getApiKeyType.ts delete mode 100644 packages/cli-v3/src/utilities/keyValueBy.ts delete mode 100644 packages/cli-v3/src/utilities/obfuscateApiKey.ts delete mode 100644 packages/cli-v3/src/utilities/parseNameAndPath.ts delete mode 100644 packages/cli-v3/src/utilities/resolveInternalFilePath.ts delete mode 100644 packages/cli-v3/src/utilities/safeJsonParse.ts delete mode 100644 packages/cli-v3/src/utilities/taskFiles.ts delete mode 100644 packages/core/src/debounce.ts delete mode 100644 packages/core/src/v3/utils/safeAsyncLocalStorage.ts diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 48575827b39..7499aac2e75 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -36,3 +36,6 @@ jobs: - name: 🔎 Lint run: pnpm exec oxlint . + + - name: ✂️ Check unused code and dependencies + run: pnpm run knip diff --git a/AGENTS.md b/AGENTS.md index 2a2dea78b55..20de6b692e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,6 +81,18 @@ pnpm run lint:fix # oxlint — auto-fixes lint violations pnpm run lint # oxlint — check only (no fixes) ``` +### Dead code + +We use knip to control unused dependencies and code. It is enforced by CI `code-quality`. + +Scan your code before pushing with: + +```bash +pnpm run knip +``` + +If there are false positives, edit ./knip.json so that it passes. + ### Imports **Prefer static imports over dynamic imports.** Only use dynamic `import()` when: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 591828c1f79..a7fae66cde1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -238,10 +238,11 @@ This never affects correctness — CI enforces the same checks on every PR; the 1. **Always open your PR in draft status first.** Do not mark it as "Ready for Review" until the steps below are complete. 2. **Run format and lint locally before pushing:** ```bash - pnpm run format # auto-fixes formatting (oxfmt) - pnpm run lint:fix # auto-fixes lint violations (oxlint) + pnpm run format + pnpm run lint + pnpm run knip ``` - Both are enforced by CI — the `code-quality` check will fail if either produces a diff or errors. + These are enforced by CI — the `code-quality` check will fail if either produces a diff or errors. 3. **Address all CodeRabbit code review comments.** Our CI runs an automated code review via CodeRabbit. Go through each comment and either fix the issue or resolve it with a comment explaining why no change is needed. 4. **Wait for all CI checks to pass.** Do not mark the PR as "Ready for Review" until every check is green. 5. **Then mark the PR as "Ready for Review"** so a maintainer can take a look. diff --git a/apps/supervisor/src/backpressure/backpressureMonitor.ts b/apps/supervisor/src/backpressure/backpressureMonitor.ts index aa16fdeaa60..b41601f76fa 100644 --- a/apps/supervisor/src/backpressure/backpressureMonitor.ts +++ b/apps/supervisor/src/backpressure/backpressureMonitor.ts @@ -1,6 +1,6 @@ import type { BackpressureMetrics } from "./backpressureMetrics.js"; -export interface BackpressureLogger { +interface BackpressureLogger { info(message: string, meta?: Record): void; error(message: string, meta?: Record): void; } diff --git a/apps/supervisor/src/clients/kubernetes.ts b/apps/supervisor/src/clients/kubernetes.ts index 1e511a68e6c..7ffb4fec204 100644 --- a/apps/supervisor/src/clients/kubernetes.ts +++ b/apps/supervisor/src/clients/kubernetes.ts @@ -3,7 +3,7 @@ import type { Informer, KubernetesObject, ListPromise } from "@kubernetes/client import { assertExhaustive } from "@trigger.dev/core/utils"; import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger"; -export const RUNTIME_ENV = process.env.KUBERNETES_PORT ? "kubernetes" : "local"; +const RUNTIME_ENV = process.env.KUBERNETES_PORT ? "kubernetes" : "local"; const logger = new SimpleStructuredLogger("kubernetes-client"); diff --git a/apps/supervisor/src/wideEvents/index.ts b/apps/supervisor/src/wideEvents/index.ts index 6e61d85896f..e736c4753d2 100644 --- a/apps/supervisor/src/wideEvents/index.ts +++ b/apps/supervisor/src/wideEvents/index.ts @@ -7,18 +7,14 @@ * Off by default behind a kill switch - the dispatch hotpath runs at high * QPS, so logging pressure must be cleanly removable. */ -export { type Env, isValidRequestId, newState, type NewStateOptions } from "./new.js"; -export { emit, EmitMessage } from "./emit.js"; -export { parseTraceId } from "./traceparent.js"; -export { fromContext, wideEventStorage } from "./context.js"; -export { type PhaseOpt, recordPhase, recordPhaseSince, timePhase } from "./record.js"; +export { fromContext } from "./context.js"; +export { recordPhaseSince } from "./record.js"; export { emitOneShot, runWideEvent, setExtra, setMeta, - type WideEventLifecycleOptions, type WideEventOptions, } from "./middleware.js"; -export type { ErrorInfo, PhaseRecord, State } from "./state.js"; +export type { State } from "./state.js"; export { encodeBaggage } from "./baggage.js"; diff --git a/apps/supervisor/src/wideEvents/state.ts b/apps/supervisor/src/wideEvents/state.ts index dece3a3f5fd..f310921aa51 100644 --- a/apps/supervisor/src/wideEvents/state.ts +++ b/apps/supervisor/src/wideEvents/state.ts @@ -76,7 +76,7 @@ export type PhaseRecord = { }; /** Top-level error summary for a failed operation. */ -export type ErrorInfo = { +type ErrorInfo = { code: string; message: string; /** Coarse classification - "client" | "upstream" | "internal" | "timeout". */ diff --git a/apps/supervisor/src/workloadToken.ts b/apps/supervisor/src/workloadToken.ts index d28a6150744..dfac2fbd2e5 100644 --- a/apps/supervisor/src/workloadToken.ts +++ b/apps/supervisor/src/workloadToken.ts @@ -28,7 +28,6 @@ const mintCounter = new Counter({ }); export type WorkloadAuthTransport = "http" | "ws"; -export type WorkloadAuthOutcome = "jwt_valid" | "jwt_invalid" | "legacy_bare" | "token_absent"; const verifyCounter = new Counter({ name: "workload_auth_verify_total", diff --git a/apps/webapp/app/assets/logos/ATAndTLogo.tsx b/apps/webapp/app/assets/logos/ATAndTLogo.tsx deleted file mode 100644 index 505294d3440..00000000000 --- a/apps/webapp/app/assets/logos/ATAndTLogo.tsx +++ /dev/null @@ -1,21 +0,0 @@ -export function ATAndTLogo({ className }: { className?: string }) { - return ( - - - - - ); -} diff --git a/apps/webapp/app/assets/logos/AstroLogo.tsx b/apps/webapp/app/assets/logos/AstroLogo.tsx deleted file mode 100644 index fb51a8f422b..00000000000 --- a/apps/webapp/app/assets/logos/AstroLogo.tsx +++ /dev/null @@ -1,57 +0,0 @@ -export function AstroLogo({ className }: { className?: string }) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/apps/webapp/app/assets/logos/ExpressLogo.tsx b/apps/webapp/app/assets/logos/ExpressLogo.tsx deleted file mode 100644 index 974e93a718a..00000000000 --- a/apps/webapp/app/assets/logos/ExpressLogo.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export function ExpressLogo({ className }: { className?: string }) { - return ( - - - - - - ); -} diff --git a/apps/webapp/app/assets/logos/FastifyLogo.tsx b/apps/webapp/app/assets/logos/FastifyLogo.tsx deleted file mode 100644 index 928df8d6a64..00000000000 --- a/apps/webapp/app/assets/logos/FastifyLogo.tsx +++ /dev/null @@ -1,45 +0,0 @@ -export function FastifyLogo({ className }: { className?: string }) { - return ( - - - - - - - - - - - - - - - - - - ); -} diff --git a/apps/webapp/app/assets/logos/NestjsLogo.tsx b/apps/webapp/app/assets/logos/NestjsLogo.tsx deleted file mode 100644 index d908241d092..00000000000 --- a/apps/webapp/app/assets/logos/NestjsLogo.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export function NestjsLogo({ className }: { className?: string }) { - return ( - - - - - ); -} diff --git a/apps/webapp/app/assets/logos/NextjsLogo.tsx b/apps/webapp/app/assets/logos/NextjsLogo.tsx deleted file mode 100644 index 9e5fa09cc0d..00000000000 --- a/apps/webapp/app/assets/logos/NextjsLogo.tsx +++ /dev/null @@ -1,47 +0,0 @@ -export function NextjsLogo({ className }: { className?: string }) { - return ( - - - - - - - - - - - - - - - - - - ); -} diff --git a/apps/webapp/app/assets/logos/NuxtLogo.tsx b/apps/webapp/app/assets/logos/NuxtLogo.tsx deleted file mode 100644 index e4fe0295bc0..00000000000 --- a/apps/webapp/app/assets/logos/NuxtLogo.tsx +++ /dev/null @@ -1,21 +0,0 @@ -export function NuxtLogo({ className }: { className?: string }) { - return ( - - - - - - - - - - - - ); -} diff --git a/apps/webapp/app/assets/logos/RedwoodLogo.tsx b/apps/webapp/app/assets/logos/RedwoodLogo.tsx deleted file mode 100644 index 6dd0e386ee5..00000000000 --- a/apps/webapp/app/assets/logos/RedwoodLogo.tsx +++ /dev/null @@ -1,42 +0,0 @@ -export function RedwoodLogo({ className }: { className?: string }) { - return ( - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/apps/webapp/app/assets/logos/RemixLogo.tsx b/apps/webapp/app/assets/logos/RemixLogo.tsx deleted file mode 100644 index be9a10fdaec..00000000000 --- a/apps/webapp/app/assets/logos/RemixLogo.tsx +++ /dev/null @@ -1,199 +0,0 @@ -export function RemixLogo({ className }: { className?: string }) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/apps/webapp/app/assets/logos/ShopifyLogo.tsx b/apps/webapp/app/assets/logos/ShopifyLogo.tsx deleted file mode 100644 index 86c71de7cfa..00000000000 --- a/apps/webapp/app/assets/logos/ShopifyLogo.tsx +++ /dev/null @@ -1,39 +0,0 @@ -export function ShopifyLogo({ className }: { className?: string }) { - return ( - - - - - - - - - - - - - ); -} diff --git a/apps/webapp/app/assets/logos/SveltekitLogo.tsx b/apps/webapp/app/assets/logos/SveltekitLogo.tsx deleted file mode 100644 index 70875e2c58c..00000000000 --- a/apps/webapp/app/assets/logos/SveltekitLogo.tsx +++ /dev/null @@ -1,29 +0,0 @@ -export function SvelteKitLogo({ className }: { className?: string }) { - return ( - - - - - - - - - - - - - - ); -} diff --git a/apps/webapp/app/assets/logos/VerizonLogo.tsx b/apps/webapp/app/assets/logos/VerizonLogo.tsx deleted file mode 100644 index 908dcb4968c..00000000000 --- a/apps/webapp/app/assets/logos/VerizonLogo.tsx +++ /dev/null @@ -1,33 +0,0 @@ -export function VerizonLogo({ className }: { className?: string }) { - return ( - - - - - - - - - - - - ); -} diff --git a/apps/webapp/app/components/BlankStatePanels.tsx b/apps/webapp/app/components/BlankStatePanels.tsx index adb5661a4b8..d2d1a4e88b0 100644 --- a/apps/webapp/app/components/BlankStatePanels.tsx +++ b/apps/webapp/app/components/BlankStatePanels.tsx @@ -3,14 +3,11 @@ import { BellAlertIcon, BookOpenIcon, ChatBubbleLeftRightIcon, - ClockIcon, PlusIcon, QuestionMarkCircleIcon, - RectangleGroupIcon, SparklesIcon, Squares2X2Icon, } from "@heroicons/react/20/solid"; -import { useLocation } from "react-use"; import { AIChatIcon } from "~/assets/icons/AIChatIcon"; import { AIPenIcon } from "~/assets/icons/AIPenIcon"; import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons"; @@ -32,7 +29,6 @@ import { v3CreateBulkActionPath, v3EnvironmentPath, v3NewProjectAlertPath, - v3NewSchedulePath, } from "~/utils/pathBuilder"; import { AskAgentButton } from "./dashboard-agent/AskAgentButton"; import { CodeBlock } from "./code/CodeBlock"; @@ -212,71 +208,6 @@ export function HasNoTasksDeployed({ environment }: { environment: MinimumEnviro return ; } -export function SchedulesNoPossibleTaskPanel() { - return ( - - How to schedule tasks - - } - > - - You have no scheduled tasks in your project. Before you can schedule a task you need to - create a schedules.task. - - - ); -} - -export function SchedulesNoneAttached() { - const organization = useOrganization(); - const project = useProject(); - const environment = useEnvironment(); - const location = useLocation(); - - return ( - - - Scheduled tasks will only run automatically if you connect a schedule to them, you can do - this in the dashboard or using the SDK. - -
- - Use the dashboard - - - Use the SDK - -
-
- ); -} - export function BatchesNone() { return ( - {children} - - - ); -} - export function BetaBadge({ inline = false, className }: { inline?: boolean; className?: string }) { return ( - {children} - - - ); -} - export function NewBadge({ inline = false, className }: { inline?: boolean; className?: string }) { return ( void; -}; - -export function GitHubLoginButton({ - label = "Continue with GitHub", - className, - onClick, -}: GitHubLoginButtonProps) { - return ( - - ); -} - export function OctoKitty({ className }: { className?: string }) { return ( ; -}) { +function GitMetadataBranch({ git }: { git: Pick }) { return ( ; @@ -62,7 +58,7 @@ export function GitMetadataCommit({ ); } -export function GitMetadataPullRequest({ +function GitMetadataPullRequest({ git, }: { git: Pick; diff --git a/apps/webapp/app/components/MachineLabelCombo.tsx b/apps/webapp/app/components/MachineLabelCombo.tsx index 485f6094cf0..29ce5e399c2 100644 --- a/apps/webapp/app/components/MachineLabelCombo.tsx +++ b/apps/webapp/app/components/MachineLabelCombo.tsx @@ -23,7 +23,7 @@ export function MachineLabelCombo({ ); } -export function MachineLabel({ +function MachineLabel({ preset, className, }: { diff --git a/apps/webapp/app/components/ProductHuntBanner.tsx b/apps/webapp/app/components/ProductHuntBanner.tsx deleted file mode 100644 index abb5a146355..00000000000 --- a/apps/webapp/app/components/ProductHuntBanner.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import productHuntLogo from "../assets/images/producthunt.png"; -import { ArrowRightIcon } from "@heroicons/react/20/solid"; -import { Paragraph } from "./primitives/Paragraph"; -import { LinkButton } from "./primitives/Buttons"; - -export function ProductHuntBanner() { - return ( -
- - We're live on{" "} - - Product Hunt - - - Vote for us today only! - -
- ); -} diff --git a/apps/webapp/app/components/SetupCommands.tsx b/apps/webapp/app/components/SetupCommands.tsx index 9b13a506deb..54dc2b65293 100644 --- a/apps/webapp/app/components/SetupCommands.tsx +++ b/apps/webapp/app/components/SetupCommands.tsx @@ -243,52 +243,6 @@ export function TriggerDevStepV3({ title }: TabsProps) { ); } -export function TriggerLoginStepV3({ title }: TabsProps) { - const triggerCliTag = useTriggerCliTag(); - const { activePackageManager, setActivePackageManager } = usePackageManager(); - - return ( - -
- {title && {title}} - - npm - pnpm - yarn - -
- - - - - - - - - -
- ); -} - export function TriggerDeployStep({ title, environment, diff --git a/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.server.ts b/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.server.ts index 7f137c61d0c..b22def305aa 100644 --- a/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.server.ts +++ b/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.server.ts @@ -12,7 +12,7 @@ import { } from "./RateLimitSection.server"; import type { EffectiveRateLimit } from "./RateLimitSection"; -export const apiRateLimitDomain: RateLimitDomain = { +const apiRateLimitDomain: RateLimitDomain = { intent: API_RATE_LIMIT_INTENT, systemDefault: () => ({ type: "tokenBucket", diff --git a/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.server.ts b/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.server.ts index 4614c5b2893..af05ace3978 100644 --- a/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.server.ts +++ b/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.server.ts @@ -12,7 +12,7 @@ import { } from "./RateLimitSection.server"; import type { EffectiveRateLimit } from "./RateLimitSection"; -export const batchRateLimitDomain: RateLimitDomain = { +const batchRateLimitDomain: RateLimitDomain = { intent: BATCH_RATE_LIMIT_INTENT, systemDefault: () => ({ type: "tokenBucket", diff --git a/apps/webapp/app/components/admin/backOffice/RateLimitSection.tsx b/apps/webapp/app/components/admin/backOffice/RateLimitSection.tsx index 09d51e69fa3..5da447ff121 100644 --- a/apps/webapp/app/components/admin/backOffice/RateLimitSection.tsx +++ b/apps/webapp/app/components/admin/backOffice/RateLimitSection.tsx @@ -12,7 +12,7 @@ import * as Property from "~/components/primitives/PropertyTable"; // view. Decoupled from the .server module so the component stays client-safe. // Duration fields are always suffixed strings — the server's DurationSchema // rejects anything else, so non-string overrides fall back to the default. -export type RateLimitConfig = +type RateLimitConfig = | { type: "tokenBucket"; refillRate: number; @@ -30,7 +30,7 @@ export type EffectiveRateLimit = { config: RateLimitConfig; }; -export type FieldErrors = Record | null; +type FieldErrors = Record | null; // Props shared by every per-domain wrapper (Api / Batch / future ones). export type RateLimitWrapperProps = { diff --git a/apps/webapp/app/components/admin/debugRun.tsx b/apps/webapp/app/components/admin/debugRun.tsx index 049c5cd08c3..6274dda3585 100644 --- a/apps/webapp/app/components/admin/debugRun.tsx +++ b/apps/webapp/app/components/admin/debugRun.tsx @@ -31,7 +31,7 @@ export function AdminDebugRun({ friendlyId }: { friendlyId: string }) { ); } -export function DebugRunDialog({ friendlyId }: { friendlyId: string }) { +function DebugRunDialog({ friendlyId }: { friendlyId: string }) { return ( 0; } -export function hasSavedAlertThresholds(alerts: BillingAlertsFormData): boolean { - return alerts.alertLevels.length > 0; -} - /** Saved thresholds that would be cleared when the billing limit alert format changes. */ export function hadSavedAlertsToClearOnLimitChange( alerts: BillingAlertsFormData, @@ -80,7 +65,7 @@ export function hadSavedAlertsToClearOnLimitChange( return hasConfiguredAlerts(alerts, billingLimit, planLimitCents); } -export function normalizeThresholdValues(values: number[]): number[] { +function normalizeThresholdValues(values: number[]): number[] { return [...values].sort((a, b) => a - b); } @@ -89,7 +74,7 @@ export function thresholdValuesAreUnique(values: number[]): boolean { return new Set(normalized).size === normalized.length; } -export function normalizeEmailValues(values: string[]): string[] { +function normalizeEmailValues(values: string[]): string[] { return values.map((value) => value.trim()).filter(Boolean); } @@ -235,33 +220,6 @@ export function isLegacyDollarAmountField( return rawAmount === planDollars || rawAmount === effectiveDollars; } -export function isAbsoluteSavedAlerts(alerts: BillingAlertsFormData): boolean { - return getSavedAlertAmountCents(alerts) === ABSOLUTE_ALERT_BASE_CENTS; -} - -/** Build a cleaned alerts payload when saving billing limits in the same alert format. */ -export function buildCleanedAlertsPayloadForLimitSave( - alerts: BillingAlertsFormData, - nextMode: BillingLimitMode, - effectiveLimitCents: number, - planLimitCents: number -): { amount: number; alertLevels: number[]; emails: string[] } | null { - if (alerts.alertLevels.length === 0) { - return null; - } - - const thresholds = storedAlertsToThresholds( - alerts, - nextMode, - effectiveLimitCents, - planLimitCents - ); - - return { - emails: alerts.emails, - ...thresholdsToAlertPayload(thresholds, nextMode, effectiveLimitCents), - }; -} /** Convert stored percentage alert levels to UI percent values (10, 50, 80). */ export function percentageAlertLevelsToUiThresholds(levels: number[]): number[] { @@ -386,10 +344,6 @@ export function thresholdsToAlertPayload( }; } -export function isEmptyThreshold(value: number): boolean { - return !Number.isFinite(value) || value <= 0; -} - export function previewDollarAmountForPercent( percent: number, effectiveLimitCents: number diff --git a/apps/webapp/app/components/code/CodeBlock.tsx b/apps/webapp/app/components/code/CodeBlock.tsx index 1eb2828c993..ee1005eceaf 100644 --- a/apps/webapp/app/components/code/CodeBlock.tsx +++ b/apps/webapp/app/components/code/CodeBlock.tsx @@ -444,7 +444,7 @@ function Chrome({ title }: { title?: string }) { ); } -export function TitleRow({ title }: { title: ReactNode }) { +function TitleRow({ title }: { title: ReactNode }) { return (
diff --git a/apps/webapp/app/components/code/InstallPackages.tsx b/apps/webapp/app/components/code/InstallPackages.tsx deleted file mode 100644 index 791d101daa9..00000000000 --- a/apps/webapp/app/components/code/InstallPackages.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { - ClientTabs, - ClientTabsList, - ClientTabsTrigger, - ClientTabsContent, -} from "../primitives/ClientTabs"; -import { ClipboardField } from "../primitives/ClipboardField"; - -type InstallPackagesProps = { - packages: string[]; -}; - -export function InstallPackages({ packages }: InstallPackagesProps) { - return ( - - - npm - pnpm - yarn - - - - - - - - - - - - ); -} diff --git a/apps/webapp/app/components/code/tsql/index.ts b/apps/webapp/app/components/code/tsql/index.ts deleted file mode 100644 index 71c543161d8..00000000000 --- a/apps/webapp/app/components/code/tsql/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// TSQL CodeMirror support -// Provides syntax highlighting, autocompletion, and linting for TSQL queries - -export { createTSQLCompletion } from "./tsqlCompletion"; -export { - createTSQLLinter, - isValidTSQLQuery, - getTSQLError, - type TSQLLinterConfig, -} from "./tsqlLinter"; diff --git a/apps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsx b/apps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsx index c83cc335700..4892352356f 100644 --- a/apps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsx +++ b/apps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsx @@ -17,17 +17,15 @@ import { } from "./suggested-prompts"; // The only slot-to-button-style mapping: a new slot is styled here and nowhere else. -export const PROMPT_SLOT_BUTTON: Record< - ResolvedPromptSlot, - { variant: ButtonVariant; icon: RenderIcon } -> = { - promoted: { variant: "primary/small", icon: SparklesIcon }, - investigate: { variant: "primary/small", icon: MagnifyingGlassIcon }, - watch: { variant: "secondary/small", icon: EyeIcon }, - status: { variant: "secondary/small", icon: ChartBarIcon }, - explain: { variant: "tertiary/small", icon: QuestionMarkCircleIcon }, - docs: { variant: "docs/small", icon: BookOpenIcon }, -}; +const PROMPT_SLOT_BUTTON: Record = + { + promoted: { variant: "primary/small", icon: SparklesIcon }, + investigate: { variant: "primary/small", icon: MagnifyingGlassIcon }, + watch: { variant: "secondary/small", icon: EyeIcon }, + status: { variant: "secondary/small", icon: ChartBarIcon }, + explain: { variant: "tertiary/small", icon: QuestionMarkCircleIcon }, + docs: { variant: "docs/small", icon: BookOpenIcon }, + }; // This surface never writes dismissals; only the row surfaces do. export function DashboardAgentSuggestedPrompts({ diff --git a/apps/webapp/app/components/dashboard-agent/agent-identity.ts b/apps/webapp/app/components/dashboard-agent/agent-identity.ts index b060bafebae..f43756d1eb5 100644 --- a/apps/webapp/app/components/dashboard-agent/agent-identity.ts +++ b/apps/webapp/app/components/dashboard-agent/agent-identity.ts @@ -1,7 +1,7 @@ import { ChatBubbleLeftRightIcon } from "@heroicons/react/20/solid"; // TODO(TRI-12763): swap in the final character icon here. -export const AGENT_NAME = "Trigger"; +const AGENT_NAME = "Trigger"; export const ASK_AGENT_LABEL = `Ask ${AGENT_NAME}`; diff --git a/apps/webapp/app/components/dashboard-agent/chat-layout.test.ts b/apps/webapp/app/components/dashboard-agent/chat-layout.test.ts index cb268f7f924..8b037e80622 100644 --- a/apps/webapp/app/components/dashboard-agent/chat-layout.test.ts +++ b/apps/webapp/app/components/dashboard-agent/chat-layout.test.ts @@ -77,8 +77,6 @@ describe("chat-layout enforcement", () => { "ChatText", "ChatCardSlot", "ChatProgress", - "ChatToolRow", - "ChatNote", "ChatStatusLine", "ChatWakeSlot", "ChatActionsRow", diff --git a/apps/webapp/app/components/dashboard-agent/chat-layout.tsx b/apps/webapp/app/components/dashboard-agent/chat-layout.tsx index d281f64b6a5..298965d9295 100644 --- a/apps/webapp/app/components/dashboard-agent/chat-layout.tsx +++ b/apps/webapp/app/components/dashboard-agent/chat-layout.tsx @@ -116,24 +116,6 @@ export function ChatProgress({ children }: { children: React.ReactNode }) { ); } -export function ChatToolRow({ children }: { children: React.ReactNode }) { - return
{children}
; -} - -export function ChatNote({ children }: { children: React.ReactNode }) { - const insetClass = useInsetClass(); - return ( -
- {children} -
- ); -} - export function ChatStatusLine({ icon, children, diff --git a/apps/webapp/app/components/dashboard-agent/dashboardAgentOpenRequest.ts b/apps/webapp/app/components/dashboard-agent/dashboardAgentOpenRequest.ts index c63342330e7..6c49bdf60d8 100644 --- a/apps/webapp/app/components/dashboard-agent/dashboardAgentOpenRequest.ts +++ b/apps/webapp/app/components/dashboard-agent/dashboardAgentOpenRequest.ts @@ -4,7 +4,7 @@ import { useSearchParams } from "@remix-run/react"; // Module-level bridge: `DashboardAgentProvider` is mounted by the environment layout, so // callers above it cannot reach the agent through context. -export type DashboardAgentOpenRequest = { +type DashboardAgentOpenRequest = { /** Omitted just opens the panel. */ prompt?: string; }; @@ -19,7 +19,7 @@ function notifyAvailability() { } /** Returns the unsubscribe. */ -export function registerDashboardAgentHost(handler: Handler): () => void { +function registerDashboardAgentHost(handler: Handler): () => void { handlers.add(handler); notifyAvailability(); return () => { diff --git a/apps/webapp/app/components/dashboard-agent/demo/fixtures/chart.ts b/apps/webapp/app/components/dashboard-agent/demo/fixtures/chart.ts index c1a00a2632a..a2ae3c80b52 100644 --- a/apps/webapp/app/components/dashboard-agent/demo/fixtures/chart.ts +++ b/apps/webapp/app/components/dashboard-agent/demo/fixtures/chart.ts @@ -1,7 +1,7 @@ import type { OutputColumnMetadata } from "@internal/clickhouse"; import type { ChartConfiguration } from "~/components/metrics/QueryWidget"; -export const demoChartColumns: OutputColumnMetadata[] = [ +const demoChartColumns: OutputColumnMetadata[] = [ { name: "hour", type: "DateTime" }, { name: "task_identifier", type: "String" }, { name: "failures", type: "UInt64", format: "quantity" }, @@ -16,16 +16,15 @@ const SERIES: Record = { const START_MS = Date.parse("2026-07-26T23:00:00.000Z"); const HOUR_MS = 3_600_000; -export const demoChartRows: Record[] = Object.entries(SERIES).flatMap( - ([task, points]) => - points.map((failures, i) => ({ - hour: new Date(START_MS + i * HOUR_MS).toISOString(), - task_identifier: task, - failures, - })) +const demoChartRows: Record[] = Object.entries(SERIES).flatMap(([task, points]) => + points.map((failures, i) => ({ + hour: new Date(START_MS + i * HOUR_MS).toISOString(), + task_identifier: task, + failures, + })) ); -export const demoChartConfig: ChartConfiguration = { +const demoChartConfig: ChartConfiguration = { chartType: "line", xAxisColumn: "hour", yAxisColumns: ["failures"], @@ -36,7 +35,7 @@ export const demoChartConfig: ChartConfiguration = { aggregation: "sum", }; -export const demoChartTimeRange = { +const demoChartTimeRange = { from: new Date(START_MS).toISOString(), to: new Date(START_MS + 11 * HOUR_MS).toISOString(), }; diff --git a/apps/webapp/app/components/dashboard-agent/demo/fixtures/intents.ts b/apps/webapp/app/components/dashboard-agent/demo/fixtures/intents.ts index 3fea9f20950..2ba225e0b27 100644 --- a/apps/webapp/app/components/dashboard-agent/demo/fixtures/intents.ts +++ b/apps/webapp/app/components/dashboard-agent/demo/fixtures/intents.ts @@ -16,7 +16,7 @@ const demoIntent = (intent: AgentIntent, outcome: string, deepLinkLabel?: string executable: isExecutableIntent(intent), }); -export const demoNavigateToFailedRuns = demoIntent( +const demoNavigateToFailedRuns = demoIntent( { kind: "navigate", target: demoRunsUri(), @@ -30,23 +30,23 @@ export const demoNavigateToFailedRuns = demoIntent( "/runs?statuses=COMPLETED_WITH_ERROR&period=24h&tasks=send-order-receipt" ); -export const demoNavigateToRun = demoIntent( +const demoNavigateToRun = demoIntent( { kind: "navigate", target: demoRunUri(DEMO_WORLD.failedRunId) }, `Opened ${DEMO_WORLD.failedRunId}`, `/runs/${DEMO_WORLD.failedRunId}` ); -export const demoAskIntent = demoIntent( +const demoAskIntent = demoIntent( { kind: "ask", prompt: "Do you want me to watch the retry and tell you when it finishes?" }, "Asked a follow-up" ); -export const demoWatchIntent = demoIntent( +const demoWatchIntent = demoIntent( { kind: "watch", spec: demoBacklogDrainWatch.spec }, `Watching ${DEMO_WORLD.backlogQueue} · checking every 5 min for up to 6h` ); -export const demoProposeFixIntent = demoIntent( +const demoProposeFixIntent = demoIntent( { kind: "propose_fix", investigationId: "demo:investigation-order-receipt" }, "Rejected: proposing a fix isn't available yet" ); diff --git a/apps/webapp/app/components/dashboard-agent/demo/fixtures/investigation.ts b/apps/webapp/app/components/dashboard-agent/demo/fixtures/investigation.ts index 8b3c250633b..d621493156c 100644 --- a/apps/webapp/app/components/dashboard-agent/demo/fixtures/investigation.ts +++ b/apps/webapp/app/components/dashboard-agent/demo/fixtures/investigation.ts @@ -11,9 +11,9 @@ import { demoSpanUri, } from "../ids"; -export type DemoHypothesisVerdict = "testing" | "validated" | "invalidated"; +type DemoHypothesisVerdict = "testing" | "validated" | "invalidated"; -export type DemoHypothesis = { +type DemoHypothesis = { id: string; statement: string; verdict: DemoHypothesisVerdict; @@ -21,11 +21,11 @@ export type DemoHypothesis = { evidence: Evidence[]; }; -export type DemoInvestigationOutcome = "in_progress" | "concluded" | "inconclusive"; +type DemoInvestigationOutcome = "in_progress" | "concluded" | "inconclusive"; -export type DemoInvestigationSeverity = "info" | "warn" | "crit"; +type DemoInvestigationSeverity = "info" | "warn" | "crit"; -export type DemoInvestigationCaveat = { +type DemoInvestigationCaveat = { kind: "dirty_commit"; message: string; }; @@ -142,7 +142,7 @@ export const demoInvestigationStreamingRev0: DemoInvestigation = { updatedAt: "2026-07-27T10:14:06.000Z", }; -export const demoInvestigationEarly: DemoInvestigation = { +const demoInvestigationEarly: DemoInvestigation = { investigationId: demoId("investigation-order-receipt-early"), revision: 0, outcome: "in_progress", @@ -249,7 +249,7 @@ export const demoInvestigationConcluded: DemoInvestigation = { updatedAt: "2026-07-27T10:14:24.000Z", }; -export const demoInvestigationConcludedNoCode: DemoInvestigation = { +const demoInvestigationConcludedNoCode: DemoInvestigation = { investigationId: demoId("investigation-queue-saturation"), revision: 2, outcome: "concluded", @@ -361,7 +361,7 @@ export const demoInvestigationInconclusive: DemoInvestigation = { updatedAt: "2026-07-27T09:41:38.000Z", }; -export const demoInvestigationDegraded: DemoInvestigation = { +const demoInvestigationDegraded: DemoInvestigation = { investigationId: demoId("investigation-order-receipt-degraded"), revision: 1, outcome: "inconclusive", diff --git a/apps/webapp/app/components/dashboard-agent/demo/fixtures/messages.ts b/apps/webapp/app/components/dashboard-agent/demo/fixtures/messages.ts index 8cfc119170a..2b6fd1d8459 100644 --- a/apps/webapp/app/components/dashboard-agent/demo/fixtures/messages.ts +++ b/apps/webapp/app/components/dashboard-agent/demo/fixtures/messages.ts @@ -4,7 +4,7 @@ import { demoId } from "../ids"; type Part = UIMessage["parts"][number]; -export function demoMessageId(name: string): string { +function demoMessageId(name: string): string { return demoId(`msg-${name}`); } diff --git a/apps/webapp/app/components/dashboard-agent/demo/fixtures/page-context.ts b/apps/webapp/app/components/dashboard-agent/demo/fixtures/page-context.ts index 14f58b61692..23e2e0da9c1 100644 --- a/apps/webapp/app/components/dashboard-agent/demo/fixtures/page-context.ts +++ b/apps/webapp/app/components/dashboard-agent/demo/fixtures/page-context.ts @@ -13,20 +13,20 @@ export const demoFreshFailureSignal: AgentPageSignal = { failedAt: "2026-07-27T10:13:41.000Z", }; -export const demoWaitingRunSignal: AgentPageSignal = { +const demoWaitingRunSignal: AgentPageSignal = { kind: "waiting_run", runId: DEMO_WORLD.waitingRunId, queue: DEMO_WORLD.queue, }; -export const demoSlowRunSignal: AgentPageSignal = { +const demoSlowRunSignal: AgentPageSignal = { kind: "slow_run", runId: DEMO_WORLD.slowRunId, durationMs: 1_421_000, baselineP95Ms: 183_000, }; -export const demoConcurrencySaturationSignal: AgentPageSignal = { +const demoConcurrencySaturationSignal: AgentPageSignal = { kind: "concurrency_saturation", severity: "crit", }; @@ -50,7 +50,7 @@ export const demoFailedRunPageContext: AgentPageContext = { signals: [demoFreshFailureSignal], }; -export const demoWaitingRunPageContext: AgentPageContext = { +const demoWaitingRunPageContext: AgentPageContext = { page: { kind: "run", runId: DEMO_WORLD.waitingRunId, @@ -61,7 +61,7 @@ export const demoWaitingRunPageContext: AgentPageContext = { signals: [demoWaitingRunSignal, demoConcurrencySaturationSignal], }; -export const demoSlowRunPageContext: AgentPageContext = { +const demoSlowRunPageContext: AgentPageContext = { page: { kind: "run", runId: DEMO_WORLD.slowRunId, @@ -71,27 +71,27 @@ export const demoSlowRunPageContext: AgentPageContext = { signals: [demoSlowRunSignal], }; -export const demoRunsPageContext: AgentPageContext = { +const demoRunsPageContext: AgentPageContext = { page: { kind: "runs", filters: { statuses: ["COMPLETED_WITH_ERROR"], period: "24h" } }, signals: [demoFreshFailureSignal], }; -export const demoErrorPageContext: AgentPageContext = { +const demoErrorPageContext: AgentPageContext = { page: { kind: "error", fingerprint: DEMO_WORLD.errorFingerprint }, signals: [demoFreshFailureSignal], }; -export const demoQueuePageContext: AgentPageContext = { +const demoQueuePageContext: AgentPageContext = { page: { kind: "queue", name: DEMO_WORLD.queue, health: "crit" }, signals: [demoConcurrencySaturationSignal, demoWaitingRunSignal], }; -export const demoDeploymentPageContext: AgentPageContext = { +const demoDeploymentPageContext: AgentPageContext = { page: { kind: "deployment", version: DEMO_WORLD.deploymentVersion }, signals: [], }; -export const demoOtherPageContext: AgentPageContext = { +const demoOtherPageContext: AgentPageContext = { page: { kind: "other", path: "/orgs/demo/projects/demo/env/prod/settings" }, signals: [], }; @@ -247,11 +247,3 @@ export const demoResolvedDismissedPromptIds: string[] = ["sp:fresh-failure"]; export const demoPromptsAfterDismissal: SuggestedPrompt[] = demoPromptSets.failedRun .filter((p) => !demoDismissedPromptIds.includes(p.id)) .slice(0, SUGGESTED_PROMPT_CAP); - -export const demoPrompts = { - sets: demoPromptSets, - defaults: DEFAULT_PROMPTS, - dismissedIds: demoDismissedPromptIds, - afterDismissal: demoPromptsAfterDismissal, - cap: SUGGESTED_PROMPT_CAP, -} as const; diff --git a/apps/webapp/app/components/dashboard-agent/demo/fixtures/watches.ts b/apps/webapp/app/components/dashboard-agent/demo/fixtures/watches.ts index 1b2367a123a..bc83fbc0d40 100644 --- a/apps/webapp/app/components/dashboard-agent/demo/fixtures/watches.ts +++ b/apps/webapp/app/components/dashboard-agent/demo/fixtures/watches.ts @@ -34,7 +34,7 @@ const watch = ( cancellable: status === "active", }); -export const demoRunFinishedWatch = watch( +const demoRunFinishedWatch = watch( "run-finished", { kind: "run_finished", @@ -64,7 +64,7 @@ export const demoBacklogDrainWatch = watch( "2026-07-27T15:02:00.000Z" ); -export const demoErrorRecurrenceWatch = watch( +const demoErrorRecurrenceWatch = watch( "email-sends", { kind: "error_recurrence", @@ -80,7 +80,7 @@ export const demoErrorRecurrenceWatch = watch( "2026-07-27T10:40:00.000Z" ); -export const demoHealthRecoveryWatch = watch( +const demoHealthRecoveryWatch = watch( "health-recovery", { kind: "health_recovery", @@ -96,7 +96,7 @@ export const demoHealthRecoveryWatch = watch( "2026-07-27T08:20:00.000Z" ); -export const demoCancelledWatch = watch( +const demoCancelledWatch = watch( "run-start", { kind: "run_start", @@ -111,7 +111,7 @@ export const demoCancelledWatch = watch( "2026-07-27T11:01:00.000Z" ); -export const demoWatchRow: DemoWatch[] = [ +const demoWatchRow: DemoWatch[] = [ demoRunFinishedWatch, demoBacklogDrainWatch, demoErrorRecurrenceWatch, @@ -119,7 +119,7 @@ export const demoWatchRow: DemoWatch[] = [ demoCancelledWatch, ]; -export const demoActiveWatchRow: DemoWatch[] = [demoRunFinishedWatch, demoBacklogDrainWatch]; +const demoActiveWatchRow: DemoWatch[] = [demoRunFinishedWatch, demoBacklogDrainWatch]; export const demoWatchNarration = { wake: `**The retry finished.** \`${DEMO_WORLD.failedRunId}\` completed successfully 4 minutes ago, on attempt 2 — the provider accepted the request once the delay pushed it out of the rate-limit window. diff --git a/apps/webapp/app/components/dashboard-agent/demo/ids.ts b/apps/webapp/app/components/dashboard-agent/demo/ids.ts index 1db8a9fdcb1..767e05ec996 100644 --- a/apps/webapp/app/components/dashboard-agent/demo/ids.ts +++ b/apps/webapp/app/components/dashboard-agent/demo/ids.ts @@ -10,8 +10,8 @@ export function demoId(rest: string): string { return `${DEMO_ID_PREFIX}${rest}`; } -export const DEMO_PROJECT_REF = "proj_demo00000000000000"; -export const DEMO_ENVIRONMENT_ID = "env_demo00000000000000"; +const DEMO_PROJECT_REF = "proj_demo00000000000000"; +const DEMO_ENVIRONMENT_ID = "env_demo00000000000000"; const scope = { projectRef: DEMO_PROJECT_REF, environmentId: DEMO_ENVIRONMENT_ID }; @@ -53,10 +53,6 @@ export function demoSourceUri(sha: string, path: string, line?: number): Trigger }); } -export function demoInvestigationUri(investigationId: string): TriggerUri { - return formatTriggerUri({ kind: "investigation", ...scope, investigationId }); -} - export const DEMO_WORLD = { failedRunId: "run_demo0f2c91", failedSpanId: "span_demoa41b", diff --git a/apps/webapp/app/components/dashboard-agent/demo/index.ts b/apps/webapp/app/components/dashboard-agent/demo/index.ts index 62e6a04bbdc..863dc44706b 100644 --- a/apps/webapp/app/components/dashboard-agent/demo/index.ts +++ b/apps/webapp/app/components/dashboard-agent/demo/index.ts @@ -1,5 +1,5 @@ // Must stay free of server imports. `demo.test.ts` asserts that. -export { DEMO_ID_PREFIX, DEMO_MARKER, DEMO_WORLD, demoId, demoReportUri, demoRunsUri } from "./ids"; +export { DEMO_WORLD, demoReportUri } from "./ids"; export * as demoFixtures from "./fixtures"; diff --git a/apps/webapp/app/components/dashboard-agent/page-context-types.ts b/apps/webapp/app/components/dashboard-agent/page-context-types.ts index 89797f8e6b8..903e5d42112 100644 --- a/apps/webapp/app/components/dashboard-agent/page-context-types.ts +++ b/apps/webapp/app/components/dashboard-agent/page-context-types.ts @@ -1,7 +1,3 @@ // The webapp's import point for these contracts. UI code should not import from // `@internal/dashboard-agent-contracts` directly. -export type { - AgentPage, - AgentPageContext, - AgentPageSignal, -} from "@internal/dashboard-agent-contracts"; +export type { AgentPage, AgentPageContext } from "@internal/dashboard-agent-contracts"; diff --git a/apps/webapp/app/components/dashboard-agent/panel-layout.tsx b/apps/webapp/app/components/dashboard-agent/panel-layout.tsx index 8faa7569015..15f581ed310 100644 --- a/apps/webapp/app/components/dashboard-agent/panel-layout.tsx +++ b/apps/webapp/app/components/dashboard-agent/panel-layout.tsx @@ -2,7 +2,7 @@ // class change only and the open chat's transport, session and transcript survive it. import { cn } from "~/utils/cn"; -export const AGENT_FULLSCREEN_STORAGE_KEY = "tdev:dashboard-agent:fullscreen"; +const AGENT_FULLSCREEN_STORAGE_KEY = "tdev:dashboard-agent:fullscreen"; export function readAgentFullscreen(): boolean { if (typeof window === "undefined") return false; diff --git a/apps/webapp/app/components/dashboard-agent/progress-line.ts b/apps/webapp/app/components/dashboard-agent/progress-line.ts index 0fe1e8168c3..0f9c1bcd95e 100644 --- a/apps/webapp/app/components/dashboard-agent/progress-line.ts +++ b/apps/webapp/app/components/dashboard-agent/progress-line.ts @@ -6,12 +6,12 @@ export const IN_FLIGHT_TOOL_STATES = new Set(["input-streaming", "input-availabl // "thinking": submitted, nothing back yet. "working": streaming text or tool calls. export type TurnActivity = "thinking" | "working"; -export const ACTIVITY_LABELS: Record = { +const ACTIVITY_LABELS: Record = { thinking: "Thinking…", working: "Working…", }; -export type ProgressSource = "investigation" | "tool" | "activity"; +type ProgressSource = "investigation" | "tool" | "activity"; export type LiveProgress = { source: ProgressSource; label: string }; diff --git a/apps/webapp/app/components/dashboard-agent/report-sparkline.tsx b/apps/webapp/app/components/dashboard-agent/report-sparkline.tsx index c0d27bd4c7d..09e263b0357 100644 --- a/apps/webapp/app/components/dashboard-agent/report-sparkline.tsx +++ b/apps/webapp/app/components/dashboard-agent/report-sparkline.tsx @@ -19,7 +19,6 @@ import { Bar, Cell, type TooltipProps } from "recharts"; import { REPORT_LABELS, reportFooterStyle, - type ReportFooterStyle, type ReportTone, } from "~/presenters/v3/reports/report-layout"; import { ActivityBarChart } from "~/components/metrics/ActivityBarChart"; @@ -39,7 +38,7 @@ export type ReportSeverityKey = "ok" | "warn" | "crit"; // Semantic tokens, not raw palette classes: only these are remapped by the theme // layer (see tailwind.css). Keyed by tone, so a genuinely-unknown state can't // borrow a verdict's colour. -export const SEVERITY_TEXT: Record = { +const SEVERITY_TEXT: Record = { ok: "text-success", warn: "text-warning", crit: "text-error", @@ -313,14 +312,11 @@ export function ReportNoteBlock({ label, children }: { label: string; children: // surfaces classify a code the same way. `action` is a primary button, `docs` the // docs button, `reference` a text link because a button would promise an action, // and `note` is prose for an option stated rather than offered. -export { reportFooterStyle, type ReportFooterStyle }; - /** * The recovery-watch offer. No report emits it; the card adds it. Two codes * because it is phrased differently when it is the only thing on offer. */ export const FOOTER_WATCH_CODE = "watch_recovery"; -export const FOOTER_WATCH_ONLY_CODE = "watch_recovery_only"; /** A dimmed line that accompanies a row entry. */ const FOOTER_NOTE_LINES: Record = { @@ -502,7 +498,7 @@ function ReportSparkTooltip({ * at full strength and the rest recede to a tint of the same colour, so the breach * reads as one chart changing intensity rather than a second series. */ -export function ReportSparkline({ +function ReportSparkline({ points, severity, /** Minutes the whole series covers. Turns a bar into its tooltip time. */ @@ -591,21 +587,6 @@ const LABEL_CLASS = "text-xs uppercase leading-tight tracking-wide text-text-dim /** A metric's movement against its baseline. Direction is always an arrow. */ export type ReportDelta = { text: string; dir: "up" | "down" | "flat" }; -/** - * A view model `Delta` as the row's arrow. A multiplier only reads as movement - * once it rounds past 1×; below that a metric with a baseline is flat, and one - * without a baseline has nothing to compare against. - */ -export function reportDelta( - delta: { dir: "up" | "down" | "flat"; mult?: number } | undefined, - hasBaseline: boolean -): ReportDelta | undefined { - if (delta && delta.mult !== undefined && delta.mult > 1 && delta.dir !== "flat") { - return { text: `${delta.dir === "up" ? "↑" : "↓"} ${delta.mult}×`, dir: delta.dir }; - } - return hasBaseline ? { text: "→ flat", dir: "flat" } : undefined; -} - export function ReportMetricRow({ label, value, diff --git a/apps/webapp/app/components/dashboard-agent/run-id.ts b/apps/webapp/app/components/dashboard-agent/run-id.ts index 91b37e80e73..6b63d67778a 100644 --- a/apps/webapp/app/components/dashboard-agent/run-id.ts +++ b/apps/webapp/app/components/dashboard-agent/run-id.ts @@ -1,6 +1,6 @@ // Every friendly id the platform mints is `run_` plus a lowercase alphanumeric // body; see `packages/core/src/v3/isomorphic/friendlyId.ts`. -export const RUN_FRIENDLY_ID_PATTERN = /^run_[a-z0-9]+$/i; +const RUN_FRIENDLY_ID_PATTERN = /^run_[a-z0-9]+$/i; export function isRunFriendlyId(value: string): boolean { return RUN_FRIENDLY_ID_PATTERN.test(value); diff --git a/apps/webapp/app/components/dashboard-agent/settled-transcript.ts b/apps/webapp/app/components/dashboard-agent/settled-transcript.ts index c6ae8bec890..187d67f5588 100644 --- a/apps/webapp/app/components/dashboard-agent/settled-transcript.ts +++ b/apps/webapp/app/components/dashboard-agent/settled-transcript.ts @@ -73,7 +73,7 @@ export function transcriptLooksUnfinished(messages: ReadonlyArray): boo * closes, so the first re-read can legitimately land before it. Retry a few times, * then leave it: a reload and the between-turns sweep are both still backstops. */ -export const SETTLE_REFETCH_DELAYS_MS = [200, 800, 2_500]; +const SETTLE_REFETCH_DELAYS_MS = [200, 800, 2_500]; export async function pollSettledTranscript(deps: { fetchTranscript: () => Promise; diff --git a/apps/webapp/app/components/dashboard-agent/suggested-prompts/dismissal.ts b/apps/webapp/app/components/dashboard-agent/suggested-prompts/dismissal.ts index 7a99bb10798..cfcafc7f02e 100644 --- a/apps/webapp/app/components/dashboard-agent/suggested-prompts/dismissal.ts +++ b/apps/webapp/app/components/dashboard-agent/suggested-prompts/dismissal.ts @@ -2,8 +2,6 @@ // different chips can't clobber each other's write. const KEY_PREFIX = "tdev:dashboard-agent:prompt-dismissed:"; -export const dismissedPromptStorageKey = (promptId: string) => `${KEY_PREFIX}${promptId}`; - export function readDismissedPromptIds(): string[] { if (typeof window === "undefined") return []; try { @@ -17,12 +15,3 @@ export function readDismissedPromptIds(): string[] { return []; } } - -export function writeDismissedPromptId(promptId: string): void { - if (typeof window === "undefined") return; - try { - window.localStorage.setItem(dismissedPromptStorageKey(promptId), "1"); - } catch { - /* storage full or blocked — the dismissal just doesn't persist */ - } -} diff --git a/apps/webapp/app/components/dashboard-agent/suggested-prompts/index.ts b/apps/webapp/app/components/dashboard-agent/suggested-prompts/index.ts index 042e7f0a566..ca1e0538fc8 100644 --- a/apps/webapp/app/components/dashboard-agent/suggested-prompts/index.ts +++ b/apps/webapp/app/components/dashboard-agent/suggested-prompts/index.ts @@ -1,27 +1,8 @@ // Client-safe only: the promoted-slot flag reader lives in `promotedPrompt.server.ts`. export { - contextualPrompts, - contextualPromptsBySlot, - formatAgo, - formatMultiplier, - GENERIC_PROMPTS, - isFailedDeploymentStatus, - pageDefaultPrompts, - pageSlotPrompts, - PROMPT_SLOTS, - promptForSignal, - SIGNAL_PRIORITY, - SIGNAL_SLOT, - type PageSlotPrompts, - type PromptSlot, -} from "./registry"; -export { - makeSuggestedPromptResolver, resolveSuggestedPrompts, resolveSuggestedPromptsBySlot, type ResolvedPromptSlot, - type ResolvedSuggestedPrompt, - type ResolveSuggestedPromptsOptions, } from "./resolver"; export { agentsAgentPageContext, @@ -35,13 +16,10 @@ export { deploymentsAgentPageContext, errorAgentPageContext, errorsAgentPageContext, - FRESH_FAILURE_WINDOW_MS, - isFailedBatchStatus, limitsAgentPageContext, modelsAgentPageContext, playgroundAgentPageContext, promptsAgentPageContext, - QUEUE_OLDEST_WAIT_WARNING_MS, queueAgentPageContext, queuesAgentPageContext, runAgentPageContext, @@ -51,11 +29,5 @@ export { taskAgentPageContext, testAgentPageContext, waitpointsAgentPageContext, - type SectionPageKind, } from "./page-mappers"; -export { parsePromotedPrompt } from "./promoted"; -export { - dismissedPromptStorageKey, - readDismissedPromptIds, - writeDismissedPromptId, -} from "./dismissal"; +export { readDismissedPromptIds } from "./dismissal"; diff --git a/apps/webapp/app/components/dashboard-agent/suggested-prompts/page-prompts.ts b/apps/webapp/app/components/dashboard-agent/suggested-prompts/page-prompts.ts index 1bc7aeb340d..bf391a427ff 100644 --- a/apps/webapp/app/components/dashboard-agent/suggested-prompts/page-prompts.ts +++ b/apps/webapp/app/components/dashboard-agent/suggested-prompts/page-prompts.ts @@ -45,7 +45,7 @@ const RUNNING_BATCH_STATUSES = new Set(["PENDING", "PROCESSING"]); /** A canceled deploy is deliberate, so it's excluded. */ const FAILED_DEPLOYMENT_STATUSES = new Set(["FAILED", "TIMED_OUT"]); -export function isFailedDeploymentStatus(status: string | undefined): boolean { +function isFailedDeploymentStatus(status: string | undefined): boolean { return status !== undefined && FAILED_DEPLOYMENT_STATUSES.has(status); } diff --git a/apps/webapp/app/components/dashboard-agent/suggested-prompts/registry.ts b/apps/webapp/app/components/dashboard-agent/suggested-prompts/registry.ts index e96ddc55bbd..ee9e3522115 100644 --- a/apps/webapp/app/components/dashboard-agent/suggested-prompts/registry.ts +++ b/apps/webapp/app/components/dashboard-agent/suggested-prompts/registry.ts @@ -5,15 +5,7 @@ * Split by responsibility; this file is the registry's public face. */ -export { PROMPT_SLOTS, type PageSlotPrompts, type PromptSlot } from "./prompt-chips"; +export { PROMPT_SLOTS, type PromptSlot } from "./prompt-chips"; export { GENERIC_PROMPTS } from "./docs-prompts"; -export { isFailedDeploymentStatus, pageDefaultPrompts, pageSlotPrompts } from "./page-prompts"; -export { - contextualPrompts, - contextualPromptsBySlot, - formatAgo, - formatMultiplier, - promptForSignal, - SIGNAL_PRIORITY, - SIGNAL_SLOT, -} from "./signal-prompts"; +export { pageDefaultPrompts, pageSlotPrompts } from "./page-prompts"; +export { contextualPromptsBySlot } from "./signal-prompts"; diff --git a/apps/webapp/app/components/dashboard-agent/suggested-prompts/signal-prompts.ts b/apps/webapp/app/components/dashboard-agent/suggested-prompts/signal-prompts.ts index 6c4a2f74982..e125c1cfc6f 100644 --- a/apps/webapp/app/components/dashboard-agent/suggested-prompts/signal-prompts.ts +++ b/apps/webapp/app/components/dashboard-agent/suggested-prompts/signal-prompts.ts @@ -10,7 +10,7 @@ import type { } from "@internal/dashboard-agent-contracts"; import { ctx, type PromptSlot } from "./prompt-chips"; -export const SIGNAL_SLOT: Record = { +const SIGNAL_SLOT: Record = { fresh_failure: "investigate", slow_run: "investigate", waiting_run: "watch", @@ -18,7 +18,7 @@ export const SIGNAL_SLOT: Record = { }; /** Signal precedence within a slot. Mirrors `demoSignalsByPriority` in the fixtures. */ -export const SIGNAL_PRIORITY: AgentPageSignalKind[] = [ +const SIGNAL_PRIORITY: AgentPageSignalKind[] = [ "fresh_failure", "waiting_run", "slow_run", @@ -26,7 +26,7 @@ export const SIGNAL_PRIORITY: AgentPageSignalKind[] = [ ]; /** "3m", "2h", "4d". */ -export function formatAgo(ms: number): string { +function formatAgo(ms: number): string { if (ms < 60_000) return "moments"; if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m`; if (ms < 86_400_000) return `${Math.round(ms / 3_600_000)}h`; @@ -34,12 +34,12 @@ export function formatAgo(ms: number): string { } /** "2.4x" under 10x, "31x" above. */ -export function formatMultiplier(factor: number): string { +function formatMultiplier(factor: number): string { return factor < 10 ? `${factor.toFixed(1)}x` : `${Math.round(factor)}x`; } /** Undefined when the signal lacks the data to say anything, e.g. a `slow_run` with no baseline. */ -export function promptForSignal(signal: AgentPageSignal, now: number): SuggestedPrompt | undefined { +function promptForSignal(signal: AgentPageSignal, now: number): SuggestedPrompt | undefined { switch (signal.kind) { case "fresh_failure": { const failedAt = Date.parse(signal.failedAt); @@ -81,19 +81,6 @@ export function promptForSignal(signal: AgentPageSignal, now: number): Suggested } } -/** In precedence order. */ -export function contextualPrompts(context: AgentPageContext, now: number): SuggestedPrompt[] { - const prompts: SuggestedPrompt[] = []; - for (const kind of SIGNAL_PRIORITY) { - for (const signal of context.signals) { - if (signal.kind !== kind) continue; - const prompt = promptForSignal(signal, now); - if (prompt) prompts.push(prompt); - } - } - return prompts; -} - /** Each group is in precedence order. */ export function contextualPromptsBySlot( context: AgentPageContext, diff --git a/apps/webapp/app/components/dashboard-agent/tooltip-accessible-name.test.ts b/apps/webapp/app/components/dashboard-agent/tooltip-accessible-name.test.ts index daa254916d0..361d20c456e 100644 --- a/apps/webapp/app/components/dashboard-agent/tooltip-accessible-name.test.ts +++ b/apps/webapp/app/components/dashboard-agent/tooltip-accessible-name.test.ts @@ -61,7 +61,6 @@ const NO_AS_CHILD_BASELINE = new Set([ "app/components/code/TSQLResultsTable.tsx::TextLink", "app/components/integrations/VercelLink.tsx::LinkButton", "app/components/primitives/CopyButton.tsx::Button", - "app/components/primitives/LabelValueStack.tsx::a", "app/components/runs/v3/RunTag.tsx::Link", "app/components/runs/v3/TaskRunsTable.tsx::DialogTrigger", "app/routes/account.tokens/route.tsx::DialogTrigger", diff --git a/apps/webapp/app/components/dashboard-agent/wake-poll.ts b/apps/webapp/app/components/dashboard-agent/wake-poll.ts index 54031977fae..c35a6d33582 100644 --- a/apps/webapp/app/components/dashboard-agent/wake-poll.ts +++ b/apps/webapp/app/components/dashboard-agent/wake-poll.ts @@ -6,7 +6,7 @@ export const UNREAD_POLL_INTERVAL_MS = 60_000; // Added to each delay so open tabs never settle into polling on the same second. -export const UNREAD_POLL_JITTER_MS = 15_000; +const UNREAD_POLL_JITTER_MS = 15_000; /** * Which of the feed's wakes this tab should toast. The feed is recent deliveries, not diff --git a/apps/webapp/app/components/dashboard-agent/watch-chips.ts b/apps/webapp/app/components/dashboard-agent/watch-chips.ts index c8de7a3eed9..19ccd5b1a80 100644 --- a/apps/webapp/app/components/dashboard-agent/watch-chips.ts +++ b/apps/webapp/app/components/dashboard-agent/watch-chips.ts @@ -18,7 +18,7 @@ import { watchIdentityValue, } from "~/presenters/v3/dashboardAgent"; -export const WATCH_STATUS_LABEL: Record = { +const WATCH_STATUS_LABEL: Record = { active: "watching", fired: "fired", expired: "expired", diff --git a/apps/webapp/app/components/dashboard-agent/watch-recommendations.ts b/apps/webapp/app/components/dashboard-agent/watch-recommendations.ts index 4ab53c2dc90..a55657f0c1f 100644 --- a/apps/webapp/app/components/dashboard-agent/watch-recommendations.ts +++ b/apps/webapp/app/components/dashboard-agent/watch-recommendations.ts @@ -44,7 +44,7 @@ export function queueWatchRecommendation( return queueAgeWatchRecommendation(queueName); } -export function queueAgeWatchRecommendation( +function queueAgeWatchRecommendation( queueName: string, thresholdMinutes: number = WATCH_DEFAULT_QUEUE_AGE_MINUTES ): WatchSpec { diff --git a/apps/webapp/app/components/layout/MetricsLayout.tsx b/apps/webapp/app/components/layout/MetricsLayout.tsx index 9a41c4b2613..ccf5129b58a 100644 --- a/apps/webapp/app/components/layout/MetricsLayout.tsx +++ b/apps/webapp/app/components/layout/MetricsLayout.tsx @@ -48,7 +48,7 @@ type ColumnCount = 1 | 2 | 3 | 4 | 5 | 6; * the value is the number of grid columns from that breakpoint up. Pass this to `Grid` when the * tile count shouldn't drive the layout (e.g. a chart grid that is always two-up). */ -export type GridColumns = { +type GridColumns = { base?: ColumnCount; sm?: ColumnCount; md?: ColumnCount; @@ -115,7 +115,7 @@ function columnsForCount(count: number): GridColumns { * - `"regions"`: Root only bounds the height (a bare `flex` column, no scroll, no rhythm); the * page composes its own scrolling areas inside the slots. */ -export type MetricsScroll = "page" | "regions"; +type MetricsScroll = "page" | "regions"; /** A length the resizable panels accept: pixels or percent (the panel library's `Unit`). */ type PanelLength = `${number}px` | `${number}%`; @@ -298,7 +298,7 @@ function MetricsLayoutFilters({ } /** Whether a grid holds stat tiles (auto height) or charts (a fixed row height). */ -export type MetricsGridKind = "tiles" | "charts"; +type MetricsGridKind = "tiles" | "charts"; /** * A grid of tiles with the baked page gutter and grid gap. Columns are derived from the tile count @@ -360,11 +360,3 @@ export const MetricsLayout = { Content: MetricsLayoutContent, Sidebar: MetricsLayoutSidebar, }; - -export { - MetricsLayoutRoot, - MetricsLayoutFilters, - MetricsLayoutGrid, - MetricsLayoutContent, - MetricsLayoutSidebar, -}; diff --git a/apps/webapp/app/components/metrics/QueryWidget.tsx b/apps/webapp/app/components/metrics/QueryWidget.tsx index 9f821816046..1f4734e1781 100644 --- a/apps/webapp/app/components/metrics/QueryWidget.tsx +++ b/apps/webapp/app/components/metrics/QueryWidget.tsx @@ -32,7 +32,7 @@ import { } from "../primitives/Popover"; const ChartType = z.union([z.literal("bar"), z.literal("line")]); -export type ChartType = z.infer; +type ChartType = z.infer; const SortDirection = z.union([z.literal("asc"), z.literal("desc")]); export type SortDirection = z.infer; diff --git a/apps/webapp/app/components/navigation/CustomizeSidebarDialog.tsx b/apps/webapp/app/components/navigation/CustomizeSidebarDialog.tsx index d2c18442e3b..ff892b7071b 100644 --- a/apps/webapp/app/components/navigation/CustomizeSidebarDialog.tsx +++ b/apps/webapp/app/components/navigation/CustomizeSidebarDialog.tsx @@ -15,7 +15,7 @@ import { Icon, type RenderIcon } from "../primitives/Icon"; import { Input } from "../primitives/Input"; import { isItemHidden, orderByPreference } from "./sideMenuTypes"; -export type CustomizeSidebarItem = { +type CustomizeSidebarItem = { id: string; name: string; icon: RenderIcon; diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index 7af60c2a679..99cfdaa540c 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -310,7 +310,7 @@ function Branches({ * Inner content of the branches popover (list, empty states, "Manage branches" footer). Shared by * the `Branches` hover submenu and the side-menu Preview popover. */ -export function BranchesPopoverContent({ +function BranchesPopoverContent({ parentEnvironment, branchEnvironments, currentEnvironment, diff --git a/apps/webapp/app/components/navigation/sideMenuTypes.ts b/apps/webapp/app/components/navigation/sideMenuTypes.ts index 508c4121175..42849769337 100644 --- a/apps/webapp/app/components/navigation/sideMenuTypes.ts +++ b/apps/webapp/app/components/navigation/sideMenuTypes.ts @@ -20,13 +20,6 @@ export const SIDE_MENU_POPOVER_ITEM_ICON = "h-5 w-5 text-text-dimmed"; export const SIDE_MENU_POPOVER_ITEM_LABEL = "text-[0.90625rem] font-medium tracking-[-0.01em]"; /** Default top-to-bottom order of the customizable side menu sections. */ -export const DEFAULT_SECTION_ORDER: SideMenuSectionId[] = [ - "favorites", - "ai", - "metrics", - "deployments", - "manage", -]; /** * Order entries by a saved preference. Entries missing from the saved order (e.g. a section or diff --git a/apps/webapp/app/components/onboarding/TechnologyPicker.tsx b/apps/webapp/app/components/onboarding/TechnologyPicker.tsx index 7236f9fa8b9..e70af0d7030 100644 --- a/apps/webapp/app/components/onboarding/TechnologyPicker.tsx +++ b/apps/webapp/app/components/onboarding/TechnologyPicker.tsx @@ -40,7 +40,7 @@ function getPillColor(value: string): string { return pillColors[Math.abs(hash) % pillColors.length]; } -export const TECHNOLOGY_OPTIONS = [ +const TECHNOLOGY_OPTIONS = [ "Airflow", "Angular", "Anthropic", diff --git a/apps/webapp/app/components/primitives/AgentDotMatrix.tsx b/apps/webapp/app/components/primitives/AgentDotMatrix.tsx index eda3f3b6c36..63d70de3e2e 100644 --- a/apps/webapp/app/components/primitives/AgentDotMatrix.tsx +++ b/apps/webapp/app/components/primitives/AgentDotMatrix.tsx @@ -25,7 +25,7 @@ import { useThemeMode } from "~/hooks/useThemeMode"; // into it. The default playlist is sequenced so every consecutive pair of // shapes shares dots. -export const MATRIX = 5; +const MATRIX = 5; // --- shapes (5-line bitmaps: "o" = dot on) --------------------------------- @@ -97,7 +97,7 @@ export const EXTRA_FACE_SHAPES: DotShapeName[] = [ // Sequenced so every consecutive pair (including the wrap) shares dots — the // head hands off between shapes without ever jumping. -export const DEFAULT_PLAYLIST: DotShapeName[] = [ +const DEFAULT_PLAYLIST: DotShapeName[] = [ "square", "rectH", "circle", diff --git a/apps/webapp/app/components/primitives/Alert.tsx b/apps/webapp/app/components/primitives/Alert.tsx index a4dcd85c757..96c6df8ddf4 100644 --- a/apps/webapp/app/components/primitives/Alert.tsx +++ b/apps/webapp/app/components/primitives/Alert.tsx @@ -111,6 +111,5 @@ export { AlertFooter, AlertTitle, AlertDescription, - AlertAction, AlertCancel, }; diff --git a/apps/webapp/app/components/primitives/AnimatingArrow.tsx b/apps/webapp/app/components/primitives/AnimatingArrow.tsx deleted file mode 100644 index 7f9a28343ce..00000000000 --- a/apps/webapp/app/components/primitives/AnimatingArrow.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid"; -import { cn } from "~/utils/cn"; - -const variants = { - small: { - size: "size-4", - arrowHeadRight: "group-hover:translate-x-[3px]", - arrowLineRight: "h-[1.5px] w-[7px] translate-x-1 top-[calc(50%-0.5px)]", - arrowHeadLeft: "group-hover:translate-x-[3px]", - arrowLineLeft: "h-[1.5px] w-[7px] translate-x-1 top-[calc(50%-0.5px)]", - arrowHeadTopRight: - "translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]", - }, - medium: { - size: "size-[1.1rem]", - arrowHeadRight: "group-hover:translate-x-[3px]", - arrowLineRight: "h-[1.5px] w-[9px] translate-x-1 top-[calc(50%-1px)]", - arrowHeadLeft: "group-hover:translate-x-[-3px]", - arrowLineLeft: "h-[1.5px] w-[9px] translate-x-1 top-[calc(50%-1px)]", - arrowHeadTopRight: - "translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]", - }, - large: { - size: "size-6", - arrowHeadRight: "group-hover:translate-x-1", - arrowLineRight: "h-[2.3px] w-[12px] translate-x-[6px] top-[calc(50%-1px)]", - arrowHeadLeft: "group-hover:translate-x-1", - arrowLineLeft: "h-[2.3px] w-[12px] translate-x-[6px] top-[calc(50%-1px)]", - arrowHeadTopRight: - "translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]", - }, - "extra-large": { - size: "size-8", - arrowHeadRight: "group-hover:translate-x-1", - arrowLineRight: "h-[3px] w-[16px] translate-x-[8px] top-[calc(50%-1.5px)]", - arrowHeadLeft: "group-hover:translate-x-1", - arrowLineLeft: "h-[3px] w-[16px] translate-x-[8px] top-[calc(50%-1.5px)]", - arrowHeadTopRight: - "translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]", - }, -}; - -export const themes = { - dark: { - textStyle: "text-background-bright", - arrowLine: "bg-background-bright", - }, - dimmed: { - textStyle: "text-text-dimmed", - arrowLine: "bg-text-dimmed", - }, - bright: { - textStyle: "text-text-bright", - arrowLine: "bg-text-bright", - }, - primary: { - textStyle: "text-text-dimmed group-hover:text-primary", - arrowLine: "bg-text-dimmed group-hover:bg-primary", - }, - blue: { - textStyle: "text-text-dimmed group-hover:text-blue-500", - arrowLine: "bg-text-dimmed group-hover:bg-blue-500", - }, - rose: { - textStyle: "text-text-dimmed group-hover:text-rose-500", - arrowLine: "bg-text-dimmed group-hover:bg-rose-500", - }, - amber: { - textStyle: "text-text-dimmed group-hover:text-amber-500", - arrowLine: "bg-text-dimmed group-hover:bg-amber-500", - }, - apple: { - textStyle: "text-text-dimmed group-hover:text-apple-500", - arrowLine: "bg-text-dimmed group-hover:bg-apple-500", - }, - lavender: { - textStyle: "text-text-dimmed group-hover:text-lavender-500", - arrowLine: "bg-text-dimmed group-hover:bg-lavender-500", - }, -}; - -type Variants = keyof typeof variants; -type Theme = keyof typeof themes; - -type AnimatingArrowProps = { - className?: string; - variant?: Variants; - theme?: Theme; - direction?: "right" | "left" | "topRight"; -}; - -export function AnimatingArrow({ - className, - variant = "medium", - theme = "dimmed", - direction = "right", -}: AnimatingArrowProps) { - const variantStyles = variants[variant]; - const themeStyles = themes[theme]; - - return ( - - {direction === "topRight" && ( - <> - - - - - - - - - - - )} - {direction === "right" && ( - <> - - - - )} - {direction === "left" && ( - <> - - - - )} - - ); -} diff --git a/apps/webapp/app/components/primitives/Avatar.tsx b/apps/webapp/app/components/primitives/Avatar.tsx index fdd6981293a..52b2af5d9a6 100644 --- a/apps/webapp/app/components/primitives/Avatar.tsx +++ b/apps/webapp/app/components/primitives/Avatar.tsx @@ -34,8 +34,8 @@ export const AvatarData = z.discriminatedUnion("type", [ export type Avatar = z.infer; export type IconAvatar = Extract; -export type ImageAvatar = Extract; -export type LettersAvatar = Extract; +type ImageAvatar = Extract; +type LettersAvatar = Extract; export function parseAvatar(json: Prisma.JsonValue, defaultAvatar: Avatar): Avatar { if (!json || typeof json !== "object") { diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 5d6b66156fb..1394d8c803d 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -1,4 +1,4 @@ -import { Link, type LinkProps, NavLink, type NavLinkProps } from "@remix-run/react"; +import { Link, type LinkProps } from "@remix-run/react"; import React, { forwardRef, type ReactNode, @@ -520,24 +520,6 @@ export const LinkButton = ({ } }; -type NavLinkPropsType = Pick & - Omit, "className"> & { - className?: (props: { isActive: boolean; isPending: boolean }) => string | undefined; - }; -export const NavLinkButton = ({ to, className, target, ...props }: NavLinkPropsType) => { - return ( - - {({ isActive, isPending }) => ( - - )} - - ); -}; - type ExtLinkProps = JSX.IntrinsicElements["a"] & { children: React.ReactNode; className?: string; diff --git a/apps/webapp/app/components/primitives/ClientTabs.tsx b/apps/webapp/app/components/primitives/ClientTabs.tsx index 48757676d61..ebe730c174d 100644 --- a/apps/webapp/app/components/primitives/ClientTabs.tsx +++ b/apps/webapp/app/components/primitives/ClientTabs.tsx @@ -199,15 +199,4 @@ const ClientTabsContent = React.forwardRef< )); ClientTabsContent.displayName = TabsPrimitive.Content.displayName; -export type TabsProps = { - tabs: { - label: string; - value: string; - }[]; - currentValue: string; - className?: string; - layoutId: string; - variant?: Variants; -}; - export { ClientTabs, ClientTabsContent, ClientTabsList, ClientTabsTrigger }; diff --git a/apps/webapp/app/components/primitives/DateTime.tsx b/apps/webapp/app/components/primitives/DateTime.tsx index 3c1227e0c9a..40015f01e5f 100644 --- a/apps/webapp/app/components/primitives/DateTime.tsx +++ b/apps/webapp/app/components/primitives/DateTime.tsx @@ -38,7 +38,7 @@ function getServerTimeZoneSnapshot(): string { * Uses useSyncExternalStore for SSR compatibility - returns "UTC" on server, * actual timezone on client. The timezone is cached and only resolved once. */ -export function useLocalTimeZone(): string { +function useLocalTimeZone(): string { return useSyncExternalStore(subscribeToTimeZone, getTimeZoneSnapshot, getServerTimeZoneSnapshot); } @@ -47,7 +47,7 @@ export function useLocalTimeZone(): string { * Returns the timezone stored in the user's preferences cookie (from root loader), * falling back to the browser's local timezone if not set. */ -export function useUserTimeZone(): string { +function useUserTimeZone(): string { const rootData = useRouteLoaderData("root") as { timezone?: string } | undefined; const localTimeZone = useLocalTimeZone(); // Use stored timezone from cookie, or fall back to browser's local timezone @@ -204,32 +204,6 @@ export function formatUtcOffset(date: Date, timeZone: string): string { return `(UTC ${sign}${hours}${minutes ? `:${minutes.toString().padStart(2, "0")}` : ""})`; } -// New component that only shows date when it changes -export const SmartDateTime = ({ date, previousDate = null, hour12 = true }: DateTimeProps) => { - const locales = useLocales(); - const userTimeZone = useUserTimeZone(); - const realDate = typeof date === "string" ? new Date(date) : date; - const realPrevDate = previousDate - ? typeof previousDate === "string" - ? new Date(previousDate) - : previousDate - : null; - - // Check if we should show the date - const showDatePart = !realPrevDate || !isSameDay(realDate, realPrevDate); - - // Format with appropriate function - const formattedDateTime = showDatePart - ? formatSmartDateTime(realDate, userTimeZone, locales, hour12) - : formatTimeOnly(realDate, userTimeZone, locales, hour12); - - return ( - - {formattedDateTime.replace(/\s/g, String.fromCharCode(32))} - - ); -}; - // Helper function to check if two dates are on the same day function isSameDay(date1: Date, date2: Date): boolean { return ( @@ -239,26 +213,6 @@ function isSameDay(date1: Date, date2: Date): boolean { ); } -// Format with date and time -function formatSmartDateTime( - date: Date, - timeZone: string, - locales: string[], - hour12: boolean = true -): string { - return new Intl.DateTimeFormat(locales, { - month: "short", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - timeZone, - // @ts-ignore fractionalSecondDigits works in most modern browsers - fractionalSecondDigits: 3, - hour12, - }).format(date); -} - // Format time only function formatTimeOnly( date: Date, diff --git a/apps/webapp/app/components/primitives/Dialog.tsx b/apps/webapp/app/components/primitives/Dialog.tsx index b62bb01f22f..5c8934b2cf8 100644 --- a/apps/webapp/app/components/primitives/Dialog.tsx +++ b/apps/webapp/app/components/primitives/Dialog.tsx @@ -112,6 +112,4 @@ export { DialogFooter, DialogTitle, DialogDescription, - DialogPortal, - DialogOverlay, }; diff --git a/apps/webapp/app/components/primitives/FormError.tsx b/apps/webapp/app/components/primitives/FormError.tsx index 218d8449984..2f8de556e12 100644 --- a/apps/webapp/app/components/primitives/FormError.tsx +++ b/apps/webapp/app/components/primitives/FormError.tsx @@ -1,4 +1,3 @@ -import type { z } from "zod"; import { Paragraph } from "./Paragraph"; import { motion } from "framer-motion"; import { cn } from "~/utils/cn"; @@ -31,25 +30,3 @@ export function FormError({ ); } - -export function ZodFormErrors({ errors, path }: { errors: z.ZodIssue[]; path: string[] }) { - if (errors.length === 0) { - return null; - } - - const relevantErrors = errors.filter((error) => { - return error.path.join(".") === path.join("."); - }); - - if (relevantErrors.length === 0) { - return null; - } - - return ( -
- {relevantErrors.map((error, index) => ( - {error.message} - ))} -
- ); -} diff --git a/apps/webapp/app/components/primitives/Headers.tsx b/apps/webapp/app/components/primitives/Headers.tsx index 5cd3ec84559..2432987fbbe 100644 --- a/apps/webapp/app/components/primitives/Headers.tsx +++ b/apps/webapp/app/components/primitives/Headers.tsx @@ -20,8 +20,6 @@ const textColorVariants = { dimmed: "text-text-dimmed", }; -export type HeaderVariant = keyof typeof headerVariants; - type HeaderProps = { className?: string; children: React.ReactNode; diff --git a/apps/webapp/app/components/primitives/Input.tsx b/apps/webapp/app/components/primitives/Input.tsx index 5c3235a66c9..0b365fa60f1 100644 --- a/apps/webapp/app/components/primitives/Input.tsx +++ b/apps/webapp/app/components/primitives/Input.tsx @@ -64,7 +64,7 @@ const variants = { }, }; -export type InputProps = React.InputHTMLAttributes & { +type InputProps = React.InputHTMLAttributes & { variant?: keyof typeof variants; icon?: RenderIcon; iconClassName?: string; diff --git a/apps/webapp/app/components/primitives/InputOTP.tsx b/apps/webapp/app/components/primitives/InputOTP.tsx index 50b14f43c1a..ab24818f815 100644 --- a/apps/webapp/app/components/primitives/InputOTP.tsx +++ b/apps/webapp/app/components/primitives/InputOTP.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import { OTPInput, OTPInputContext } from "input-otp"; -import { MinusIcon } from "lucide-react"; import { cn } from "~/utils/cn"; @@ -99,12 +98,4 @@ function InputOTPSlot({ ); } -function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) { - return ( -
- -
- ); -} - -export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }; +export { InputOTP, InputOTPGroup, InputOTPSlot }; diff --git a/apps/webapp/app/components/primitives/LabelValueStack.tsx b/apps/webapp/app/components/primitives/LabelValueStack.tsx deleted file mode 100644 index 977ef6ee84c..00000000000 --- a/apps/webapp/app/components/primitives/LabelValueStack.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { cn } from "~/utils/cn"; -import { Paragraph } from "./Paragraph"; -import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid"; -import { SimpleTooltip } from "./Tooltip"; -import { Link } from "@remix-run/react"; - -const variations = { - primary: { - label: "extra-small/bright", - value: "extra-small", - }, - secondary: { - label: "extra-extra-small/caps", - value: "extra-small/bright", - }, -} as const; - -type LabelValueStackProps = { - label: React.ReactNode; - value: React.ReactNode; - href?: string; - layout?: "horizontal" | "vertical"; - variant?: keyof typeof variations; - className?: string; -}; - -export function LabelValueStack({ - label, - value, - href, - layout = "vertical", - variant = "secondary", - className, -}: LabelValueStackProps) { - const variation = variations[variant]; - - return ( -
- {label} - <> - {href ? ( - - ) : ( - {value} - )} - -
- ); -} - -type ValueButtonStackProps = { - value: React.ReactNode; - href: string; - variant?: keyof typeof variations; -}; - -function ValueButton({ value, href, variant = "secondary" }: ValueButtonStackProps) { - const variation = variations[variant]; - - const isExternalUrl = href.startsWith("http"); - - if (!isExternalUrl) { - return ( - - - {value} - - - ); - } - - return ( - - - {value} - - -
- } - content={href} - /> - ); -} diff --git a/apps/webapp/app/components/primitives/LoadingBarDivider.tsx b/apps/webapp/app/components/primitives/LoadingBarDivider.tsx index 713240177a5..00259d38f28 100644 --- a/apps/webapp/app/components/primitives/LoadingBarDivider.tsx +++ b/apps/webapp/app/components/primitives/LoadingBarDivider.tsx @@ -15,7 +15,7 @@ export function LoadingBarDivider({ isLoading, className }: LoadingBarDividerPro ); } -export function AnimationDivider({ isLoading }: LoadingBarDividerProps) { +function AnimationDivider({ isLoading }: LoadingBarDividerProps) { const [scope, animate] = useAnimate(); const [isPresent, safeToRemove] = usePresence(); diff --git a/apps/webapp/app/components/primitives/Popover.tsx b/apps/webapp/app/components/primitives/Popover.tsx index e0442b915fc..dcdb1076897 100644 --- a/apps/webapp/app/components/primitives/Popover.tsx +++ b/apps/webapp/app/components/primitives/Popover.tsx @@ -6,12 +6,10 @@ import * as PopoverPrimitive from "@radix-ui/react-popover"; import { Link } from "@remix-run/react"; import * as React from "react"; import { DropdownIcon } from "~/assets/icons/DropdownIcon"; -import * as useShortcutKeys from "~/hooks/useShortcutKeys"; import { cn } from "~/utils/cn"; import { ButtonContent, type ButtonContentPropsType } from "./Buttons"; import { type RenderIcon } from "./Icon"; import { Paragraph, type ParagraphVariant } from "./Paragraph"; -import { ShortcutKey } from "./ShortcutKey"; const Popover = PopoverPrimitive.Root; const PopoverTrigger = PopoverPrimitive.Trigger; @@ -163,48 +161,6 @@ function PopoverCustomTrigger({ ); } -function PopoverSideMenuTrigger({ - isOpen, - children, - className, - shortcut, - hideShortcutKey = false, - ...props -}: { - isOpen?: boolean; - shortcut?: useShortcutKeys.ShortcutDefinition; - hideShortcutKey?: boolean; -} & React.ComponentPropsWithoutRef) { - const ref = React.useRef(null); - useShortcutKeys.useShortcutKeys({ - shortcut: shortcut, - action: (e) => { - e.preventDefault(); - e.stopPropagation(); - if (ref.current) { - ref.current.click(); - } - }, - }); - - return ( - - {children} - {shortcut && !hideShortcutKey && ( - - )} - - ); -} - const popoverArrowTriggerVariants = { minimal: { trigger: "text-text-dimmed hover:bg-background-raised hover:text-text-bright", @@ -328,9 +284,6 @@ export { PopoverMenuItem, PopoverSectionHeader, PopoverEllipseTrigger, - PopoverSideMenuTrigger, PopoverTrigger, PopoverVerticalEllipseTrigger, }; - -export type { PopoverArrowTriggerVariant }; diff --git a/apps/webapp/app/components/primitives/PrettyDuration.tsx b/apps/webapp/app/components/primitives/PrettyDuration.tsx deleted file mode 100644 index b4f8a094d7f..00000000000 --- a/apps/webapp/app/components/primitives/PrettyDuration.tsx +++ /dev/null @@ -1,40 +0,0 @@ -// Formats duration in a human readable way, some examples: -// 1h 30m -// 1m 30s -// 1h -// Uses built-in plain Date object, so it's not timezone aware -export function PrettyDuration({ - startAt, - endAt, - fallback, -}: { - startAt?: Date | null; - endAt?: Date | null; - fallback?: string; -}) { - if (!startAt || !endAt) { - return <>{fallback ?? "-"}; - } - - const duration = Math.abs(endAt.getTime() - startAt.getTime()); - - const hours = Math.floor(duration / (1000 * 60 * 60)); - const minutes = Math.floor((duration / (1000 * 60)) % 60); - const seconds = Math.floor((duration / 1000) % 60); - - const durationParts = []; - - if (hours > 0) { - durationParts.push(`${hours}h`); - } - - if (minutes > 0) { - durationParts.push(`${minutes}m`); - } - - if (seconds > 0) { - durationParts.push(`${seconds}s`); - } - - return <>{durationParts.join(" ")}; -} diff --git a/apps/webapp/app/components/primitives/Select.tsx b/apps/webapp/app/components/primitives/Select.tsx index 31921ef9854..129e3396dd0 100644 --- a/apps/webapp/app/components/primitives/Select.tsx +++ b/apps/webapp/app/components/primitives/Select.tsx @@ -618,12 +618,6 @@ export function shortcutFromIndex( return { key: String(adjustedIndex + 1) }; } -export interface SelectSeparatorProps extends React.ComponentProps<"div"> {} - -export function SelectSeparator(props: SelectSeparatorProps) { - return
; -} - export interface SelectGroupProps extends Ariakit.SelectGroupProps {} export function SelectGroup(props: SelectGroupProps) { @@ -644,8 +638,8 @@ export function SelectGroupLabel(props: SelectGroupLabelProps) { ); } -export interface SelectHeadingProps extends Ariakit.SelectHeadingProps {} -export function SelectHeading({ render, ...props }: SelectHeadingProps) { +interface SelectHeadingProps extends Ariakit.SelectHeadingProps {} +function SelectHeading({ render, ...props }: SelectHeadingProps) { return (
@@ -679,9 +673,9 @@ export function SelectPopover({ ); } -export interface SelectLabelProps extends Ariakit.SelectLabelProps {} +interface SelectLabelProps extends Ariakit.SelectLabelProps {} //currently unstyled -export function SelectLabel(props: SelectLabelProps) { +function SelectLabel(props: SelectLabelProps) { return ; } diff --git a/apps/webapp/app/components/primitives/Sheet.tsx b/apps/webapp/app/components/primitives/Sheet.tsx deleted file mode 100644 index b7376245f4c..00000000000 --- a/apps/webapp/app/components/primitives/Sheet.tsx +++ /dev/null @@ -1,201 +0,0 @@ -"use client"; - -import * as SheetPrimitive from "@radix-ui/react-dialog"; -import type { VariantProps } from "class-variance-authority"; -import { cva } from "class-variance-authority"; -import * as React from "react"; -import { cn } from "~/utils/cn"; -import { ShortcutKey } from "./ShortcutKey"; -import { XMarkIcon } from "@heroicons/react/20/solid"; - -const Sheet = SheetPrimitive.Root; - -const SheetTrigger = SheetPrimitive.Trigger; - -const portalVariants = cva("fixed inset-0 z-50 flex", { - variants: { - position: { - top: "items-start", - bottom: "items-end", - left: "justify-start", - right: "justify-end", - }, - }, - defaultVariants: { position: "right" }, -}); - -interface SheetPortalProps - extends SheetPrimitive.DialogPortalProps, VariantProps {} - -const SheetPortal = ({ position, children, ...props }: SheetPortalProps) => ( - -
{children}
-
-); -SheetPortal.displayName = SheetPrimitive.Portal.displayName; - -const SheetOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - -)); -SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; - -const sheetVariants = cva( - "fixed z-50 scale-100 gap-4 shadow-lg bg-background-bright opacity-100 border-l border-grid-bright", - { - variants: { - position: { - top: "animate-in slide-in-from-top w-full duration-200", - bottom: "animate-in slide-in-from-bottom w-full duration-200", - left: "animate-in slide-in-from-left h-full duration-200", - right: "animate-in slide-in-from-right h-screen duration-200", - }, - size: { - content: "", - default: "", - sm: "", - lg: "", - xl: "", - full: "", - }, - }, - compoundVariants: [ - { - position: ["top", "bottom"], - size: "content", - class: "max-h-screen", - }, - { - position: ["top", "bottom"], - size: "default", - class: "h-1/3", - }, - { - position: ["top", "bottom"], - size: "sm", - class: "h-1/4", - }, - { - position: ["top", "bottom"], - size: "lg", - class: "h-1/2", - }, - { - position: ["top", "bottom"], - size: "xl", - class: "h-5/6", - }, - { - position: ["top", "bottom"], - size: "full", - class: "h-screen", - }, - { - position: ["right", "left"], - size: "content", - class: "max-w-screen", - }, - { - position: ["right", "left"], - size: "default", - class: "w-1/3", - }, - { - position: ["right", "left"], - size: "sm", - class: "w-1/4", - }, - { - position: ["right", "left"], - size: "lg", - class: "w-1/2", - }, - { - position: ["right", "left"], - size: "xl", - class: "w-5/6", - }, - { - position: ["right", "left"], - size: "full", - class: "w-screen", - }, - ], - defaultVariants: { - position: "right", - size: "default", - }, - } -); - -export interface DialogContentProps - extends - React.ComponentPropsWithoutRef, - VariantProps {} - -const SheetContent = React.forwardRef< - React.ElementRef, - DialogContentProps ->(({ position, size, className, children, ...props }, ref) => ( - - - -
-
- - - Close - - -
-
{children}
-
-
-
-)); -SheetContent.displayName = SheetPrimitive.Content.displayName; - -export const SheetBody = ({ className, ...props }: React.HTMLAttributes) => ( -
-); - -export const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
-); - -export const SheetFooter = ({ - className, - children, - ...props -}: React.HTMLAttributes) => ( -
-
{children}
-
-); - -export { Sheet, SheetContent, SheetTrigger }; diff --git a/apps/webapp/app/components/primitives/SheetV3.tsx b/apps/webapp/app/components/primitives/SheetV3.tsx index 5bfc14285e7..977a18ae6e7 100644 --- a/apps/webapp/app/components/primitives/SheetV3.tsx +++ b/apps/webapp/app/components/primitives/SheetV3.tsx @@ -8,8 +8,6 @@ const Sheet = SheetPrimitive.Root; const SheetTrigger = SheetPrimitive.Trigger; -const SheetClose = SheetPrimitive.Close; - const SheetPortal = SheetPrimitive.Portal; const SheetOverlay = React.forwardRef< @@ -109,15 +107,4 @@ const SheetDescription = React.forwardRef< )); SheetDescription.displayName = SheetPrimitive.Description.displayName; -export { - Sheet, - SheetClose, - SheetContent, - SheetDescription, - SheetFooter, - SheetHeader, - SheetOverlay, - SheetPortal, - SheetTitle, - SheetTrigger, -}; +export { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger }; diff --git a/apps/webapp/app/components/primitives/Table.tsx b/apps/webapp/app/components/primitives/Table.tsx index 55dd2e4e200..e0dca744935 100644 --- a/apps/webapp/app/components/primitives/Table.tsx +++ b/apps/webapp/app/components/primitives/Table.tsx @@ -1,5 +1,4 @@ import { ChevronDownIcon, ChevronUpDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid"; -import { ChevronRightIcon } from "@heroicons/react/24/solid"; import { Link } from "@remix-run/react"; import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react"; import React, { type ReactNode, createContext, forwardRef, useContext, useState } from "react"; @@ -513,31 +512,6 @@ export const CopyableTableCell = forwardRef) => void; - } ->(({ className, to, children, isSticky, onClick }, ref) => { - return ( - - {children} - - - ); -}); - export const TableCellMenu = forwardRef< HTMLTableCellElement, TableCellProps & { diff --git a/apps/webapp/app/components/primitives/Tabs.tsx b/apps/webapp/app/components/primitives/Tabs.tsx index 569e271434f..3df60f92b25 100644 --- a/apps/webapp/app/components/primitives/Tabs.tsx +++ b/apps/webapp/app/components/primitives/Tabs.tsx @@ -81,7 +81,7 @@ export function TabContainer({ return
{children}
; } -export function TabLink({ +function TabLink({ to, children, layoutId, diff --git a/apps/webapp/app/components/primitives/Timeline.tsx b/apps/webapp/app/components/primitives/Timeline.tsx index a5164b47b2b..c2eecd0758b 100644 --- a/apps/webapp/app/components/primitives/Timeline.tsx +++ b/apps/webapp/app/components/primitives/Timeline.tsx @@ -7,7 +7,7 @@ interface MousePosition { y: number; } const MousePositionContext = createContext(undefined); -export function MousePositionProvider({ children }: { children: ReactNode }) { +function MousePositionProvider({ children }: { children: ReactNode }) { const ref = useRef(null); const [position, setPosition] = useState(undefined); @@ -44,7 +44,7 @@ export function MousePositionProvider({ children }: { children: ReactNode }) {
); } -export const useMousePosition = () => { +const useMousePosition = () => { return useContext(MousePositionContext); }; diff --git a/apps/webapp/app/components/primitives/Tooltip.tsx b/apps/webapp/app/components/primitives/Tooltip.tsx index cb9eaf0364d..b2155ab362a 100644 --- a/apps/webapp/app/components/primitives/Tooltip.tsx +++ b/apps/webapp/app/components/primitives/Tooltip.tsx @@ -144,4 +144,4 @@ export function InfoIconTooltip({ ); } -export { SimpleTooltip, Tooltip, TooltipArrow, TooltipContent, TooltipProvider, TooltipTrigger }; +export { SimpleTooltip, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; diff --git a/apps/webapp/app/components/primitives/TreeView/TreeView.tsx b/apps/webapp/app/components/primitives/TreeView/TreeView.tsx index e19a006df61..dd204d0c420 100644 --- a/apps/webapp/app/components/primitives/TreeView/TreeView.tsx +++ b/apps/webapp/app/components/primitives/TreeView/TreeView.tsx @@ -26,9 +26,6 @@ export type TreeViewProps = { onScroll?: (scrollTop: number) => void; } & Pick; -export type GetTreePropsFn = UseTreeStateOutput["getTreeProps"]; -export type GetNodePropsFn = UseTreeStateOutput["getNodeProps"]; - export function TreeView({ tree, renderNode, diff --git a/apps/webapp/app/components/primitives/TreeView/utils.ts b/apps/webapp/app/components/primitives/TreeView/utils.ts index 95fc1b75614..c98aefd1e89 100644 --- a/apps/webapp/app/components/primitives/TreeView/utils.ts +++ b/apps/webapp/app/components/primitives/TreeView/utils.ts @@ -49,7 +49,7 @@ export function concreteStateFromInput({ }); } -export function concreteStateFromPartialState( +function concreteStateFromPartialState( tree: FlatTree, state: PartialNodeState ): NodesState { diff --git a/apps/webapp/app/components/primitives/charts/Chart.tsx b/apps/webapp/app/components/primitives/charts/Chart.tsx index d19452c0bc3..e816cca3758 100644 --- a/apps/webapp/app/components/primitives/charts/Chart.tsx +++ b/apps/webapp/app/components/primitives/charts/Chart.tsx @@ -252,8 +252,6 @@ const ChartTooltipContent = React.forwardRef< ); ChartTooltipContent.displayName = "ChartTooltip"; -const ChartLegend = RechartsPrimitive.Legend; - type ExtendedLegendPayload = Parameters< NonNullable >[0] & { @@ -458,12 +456,4 @@ function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]; } -export { - ChartContainer, - ChartTooltip, - ChartTooltipContent, - ChartLegend, - ChartLegendContent, - ChartLegendContentRows, - ChartStyle, -}; +export { ChartContainer, ChartTooltip, ChartTooltipContent }; diff --git a/apps/webapp/app/components/primitives/charts/ChartCompound.tsx b/apps/webapp/app/components/primitives/charts/ChartCompound.tsx index bbd78fc1b32..2f8110b376e 100644 --- a/apps/webapp/app/components/primitives/charts/ChartCompound.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartCompound.tsx @@ -70,12 +70,6 @@ import { ChartZoom } from "./ChartZoom"; // Re-export types export type { ChartConfig, ChartState } from "./Chart"; -export type { ZoomRange } from "./hooks/useZoomSelection"; -export type { ChartRootProps } from "./ChartRoot"; -export type { ChartBarRendererProps } from "./ChartBar"; -export type { ChartLineRendererProps } from "./ChartLine"; -export type { ChartLegendCompoundProps } from "./ChartLegendCompound"; -export type { ChartZoomProps } from "./ChartZoom"; /** * Chart compound component for building flexible, composable charts. @@ -98,9 +92,5 @@ export const Chart = { }; // Also export individual components for direct imports -export { ChartRoot, ChartBarRenderer, ChartLineRenderer, ChartLegendCompound, ChartZoom }; // Re-export context hook for advanced usage -export { useChartContext } from "./ChartContext"; -export { useHasNoData, useSeriesTotal } from "./ChartRoot"; -export { useZoomHandlers, ZoomTooltip } from "./ChartZoom"; diff --git a/apps/webapp/app/components/primitives/charts/DateRangeContext.tsx b/apps/webapp/app/components/primitives/charts/DateRangeContext.tsx index 4b6d8210596..36a864b6321 100644 --- a/apps/webapp/app/components/primitives/charts/DateRangeContext.tsx +++ b/apps/webapp/app/components/primitives/charts/DateRangeContext.tsx @@ -24,21 +24,21 @@ const longDateFormatter = new Intl.DateTimeFormat("en-US", { /** * Format a Date object as a short date string (e.g., "Nov 1") */ -export function formatChartDate(date: Date): string { +function formatChartDate(date: Date): string { return shortDateFormatter.format(date); } /** * Format a Date object as a long date string (e.g., "Nov 1, 2023") */ -export function formatChartDateLong(date: Date): string { +function formatChartDateLong(date: Date): string { return longDateFormatter.format(date); } /** * Convert a Date to ISO date string (YYYY-MM-DD) using local date components */ -export function toISODateString(date: Date): string { +function toISODateString(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); @@ -48,7 +48,7 @@ export function toISODateString(date: Date): string { /** * Parse an ISO date string (YYYY-MM-DD) to a local Date object */ -export function parseISODateString(isoString: string): Date { +function parseISODateString(isoString: string): Date { const [year, month, day] = isoString.split("-").map(Number); return new Date(year, month - 1, day); } diff --git a/apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts b/apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts index 02baac0ca8f..c73137484dc 100644 --- a/apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts +++ b/apps/webapp/app/components/primitives/charts/hooks/useHighlightState.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo, useState } from "react"; -export type HighlightState = { +type HighlightState = { /** The currently highlighted series key (e.g., "completed", "failed") */ activeBarKey: string | null; /** The index of the specific data point being hovered (null when hovering legend) */ @@ -9,7 +9,7 @@ export type HighlightState = { tooltipActive: boolean; }; -export type HighlightActions = { +type HighlightActions = { /** Set the hovered bar (specific data point) */ setHoveredBar: (key: string, index: number) => void; /** Set the hovered legend item (highlights all bars of that type) */ @@ -75,32 +75,3 @@ export function useHighlightState(): UseHighlightStateReturn { [state, setHoveredBar, setHoveredLegendItem, setTooltipActive, reset] ); } - -/** - * Calculate the opacity for a bar based on highlight state. - * @param key - The series key of this bar - * @param dataIndex - The data point index of this bar - * @param highlight - The current highlight state - * @param dimmedOpacity - The opacity to use for dimmed bars (default 0.2) - */ -export function getBarOpacity( - key: string, - dataIndex: number, - highlight: HighlightState, - dimmedOpacity = 0.2 -): number { - const { activeBarKey, activeDataPointIndex } = highlight; - - // No highlight active - full opacity - if (activeBarKey === null) { - return 1; - } - - // Hovering a specific bar (from chart) - if (activeDataPointIndex !== null) { - return key === activeBarKey && dataIndex === activeDataPointIndex ? 1 : dimmedOpacity; - } - - // Hovering a legend item (all bars of this type) - return key === activeBarKey ? 1 : dimmedOpacity; -} diff --git a/apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts b/apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts index 1d7110fda8f..e34846af00e 100644 --- a/apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts +++ b/apps/webapp/app/components/primitives/charts/hooks/useZoomSelection.ts @@ -5,7 +5,7 @@ export type ZoomRange = { end: string; }; -export type ZoomSelectionState = { +type ZoomSelectionState = { /** Starting point of drag selection (x-axis value) */ refAreaLeft: string | null; /** Ending point of drag selection (x-axis value) */ @@ -18,7 +18,7 @@ export type ZoomSelectionState = { inspectionLine: string | null; }; -export type ZoomSelectionActions = { +type ZoomSelectionActions = { /** Start a new selection at the given x-axis value */ startSelection: (label: string) => void; /** Update the selection as the user drags */ diff --git a/apps/webapp/app/components/primitives/charts/statusColors.ts b/apps/webapp/app/components/primitives/charts/statusColors.ts index 8439b4d3285..8fae83125fd 100644 --- a/apps/webapp/app/components/primitives/charts/statusColors.ts +++ b/apps/webapp/app/components/primitives/charts/statusColors.ts @@ -1,6 +1,6 @@ /** Shared status → color map for the task/agent activity charts. * Values are CSS variables so they follow the theme; CSS contexts only. */ -export const STATUS_COLOR: Record = { +const STATUS_COLOR: Record = { // Run-status groups COMPLETED: "var(--color-success)", RUNNING: "var(--color-pending)", @@ -12,7 +12,7 @@ export const STATUS_COLOR: Record = { EXPIRED: "var(--color-text-dimmed)", }; -export const STATUS_COLOR_FALLBACK = "var(--color-text-dimmed)"; +const STATUS_COLOR_FALLBACK = "var(--color-text-dimmed)"; export function statusColor(status: string): string { return STATUS_COLOR[status] ?? STATUS_COLOR_FALLBACK; diff --git a/apps/webapp/app/components/primitives/useTableSort.ts b/apps/webapp/app/components/primitives/useTableSort.ts index 5bb5f600431..991fb20897f 100644 --- a/apps/webapp/app/components/primitives/useTableSort.ts +++ b/apps/webapp/app/components/primitives/useTableSort.ts @@ -1,12 +1,5 @@ -import { useCallback, useMemo, useState } from "react"; - export type SortDirection = "asc" | "desc"; -export type SortState = { - key: K; - direction: SortDirection; -}; - /** * A sortable column definition for {@link useTableSort}. * @@ -24,12 +17,6 @@ export type SortColumn = | { key: K; type: "alpha"; value: (row: T) => string | null | undefined } | { key: K; type: "custom"; compare: (a: T, b: T) => number }; -/** Presentational props to spread onto a `` for a given column. */ -export type TableSortHeaderProps = { - sortDirection: SortDirection | null; - onSort: () => void; -}; - export function compareColumn( column: SortColumn, a: T, @@ -83,48 +70,3 @@ export function sortRows( }) .map((entry) => entry.row); } - -/** - * Client-side, header-click column sorting for tables of any row shape. - * - * Clicking a column cycles asc -> desc -> cleared (back to the original row order), so the - * incoming order (e.g. a server default) is always reachable without a reload. Returns the - * sorted rows plus a `getSortProps(key)` helper whose result spreads straight onto - * ``. - */ -export function useTableSort( - rows: T[], - columns: ReadonlyArray> -) { - const [sort, setSort] = useState | null>(null); - - const columnsByKey = useMemo(() => { - const map = new Map>(); - for (const column of columns) { - map.set(column.key, column); - } - return map; - }, [columns]); - - const sortedRows = useMemo(() => { - if (!sort) return rows; - const column = columnsByKey.get(sort.key); - if (!column) return rows; - return sortRows(rows, column, sort.direction); - }, [rows, sort, columnsByKey]); - - const getSortProps = useCallback( - (key: K): TableSortHeaderProps => ({ - sortDirection: sort?.key === key ? sort.direction : null, - onSort: () => - setSort((current) => { - if (!current || current.key !== key) return { key, direction: "asc" }; - if (current.direction === "asc") return { key, direction: "desc" }; - return null; - }), - }), - [sort] - ); - - return { sortedRows, getSortProps, sort }; -} diff --git a/apps/webapp/app/components/query/QueryEditor.tsx b/apps/webapp/app/components/query/QueryEditor.tsx index 1b616059231..d46093929fe 100644 --- a/apps/webapp/app/components/query/QueryEditor.tsx +++ b/apps/webapp/app/components/query/QueryEditor.tsx @@ -99,7 +99,7 @@ type QueryActionResponse = { maxQueryPeriod?: number; }; -export type QueryEditorMode = +type QueryEditorMode = | { type: "standalone" } | { type: "dashboard-add"; dashboardId: string; dashboardName: string } | { diff --git a/apps/webapp/app/components/queues/QueueMetricCards.tsx b/apps/webapp/app/components/queues/QueueMetricCards.tsx index 294a94b8650..7c87a1e2d02 100644 --- a/apps/webapp/app/components/queues/QueueMetricCards.tsx +++ b/apps/webapp/app/components/queues/QueueMetricCards.tsx @@ -6,18 +6,14 @@ import { type ChartState, } from "~/components/primitives/charts/ChartCompound"; import { ChartCard } from "~/components/primitives/charts/ChartCard"; -import { MiniLineChart } from "~/components/metrics/MiniLineChart"; import { useMetricResourceQuery, type MetricResourceTimeRange, } from "~/hooks/useMetricResourceQuery"; -import { Header3 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { InfoIconTooltip } from "~/components/primitives/Tooltip"; import { useSearchParams } from "~/hooks/useSearchParam"; import { QUEUE_METRICS_DEFAULT_PERIOD } from "~/components/queues/queueMetricsPeriod"; -import { cn } from "~/utils/cn"; -import { formatNumberCompact } from "~/utils/numberFormatter"; // Shared building blocks for queue-metric UI (queue detail page, task detail page, // run inspector). All CH-derived data is fetched client-side through useQueueMetric @@ -86,7 +82,7 @@ export function formatWaitMs(ms: number): string { return `${(ms / 3_600_000).toFixed(1)}h`; } -export type QueueMetricSeriesConfig = { key: string; label: string; color: string }; +type QueueMetricSeriesConfig = { key: string; label: string; color: string }; type QueueMetricChartProps = { query: string; @@ -367,115 +363,3 @@ export function QueueSidebarStats({ // A compact stat card with a recent trend sparkline underneath, for the run inspector. // The headline is a live "now" value from the loader; the sparkline pulls its own series. -const SPARKLINE_PERIOD = "30m"; - -export function QueueSparklineStat({ - title, - info, - query, - color, - ids, - queueName, - formatPeak, - unitLabel, - chartHeight, -}: { - title: string; - /** Tooltip text under the info icon next to the title (matches the queue page copy). */ - info?: ReactNode; - query: string; - color: string; - ids: QueueMetricIds; - queueName: string; - formatPeak?: (peak: number) => string; - /** Unit shown in the per-bucket hover tooltip (e.g. queued, ms). */ - unitLabel?: { singular: string; plural: string }; - /** Plot height in px. Defaults to the shared mini-chart height. */ - chartHeight?: number; -}) { - const timeRange: QueueMetricTimeRange = { period: SPARKLINE_PERIOD, from: null, to: null }; - const { rows } = useQueueMetric(query, { - ids, - timeRange, - queueName, - fillGaps: true, - defaultPeriod: SPARKLINE_PERIOD, - }); - - const { data, throttled, bucketStartMs, bucketIntervalMs, peak } = useMemo(() => { - const points = rows - .map((r) => ({ - bucket: clickhouseTimeToMs(r.t), - v: toNumber(r.v), - // Present only when the query selects it (Backlog); 0 elsewhere so no overlay draws. - throttled: toNumber(r.throttled), - })) - .filter((p) => Number.isFinite(p.bucket)) - .sort((a, b) => a.bucket - b.bucket); - return { - data: points.map((p) => p.v), - throttled: points.map((p) => p.throttled), - bucketStartMs: points[0]?.bucket, - bucketIntervalMs: points.length > 1 ? points[1]!.bucket - points[0]!.bucket : undefined, - peak: points.reduce((m, p) => Math.max(m, p.v), 0), - }; - }, [rows]); - - return ( -
-
- {title} - {info || (data.length > 0 && peak > 0) ? ( - - {info ? {info} : null} - {data.length > 0 && peak > 0 ? ( - - Peak {formatPeak ? formatPeak(peak) : formatNumberCompact(peak)} - - ) : null} -
- } - contentClassName="max-w-[230px]" - disableHoverableContent - /> - ) : null} -
- -
- ); -} - -export function QueueMetricStat({ - label, - value, - className, - loading, -}: { - label: string; - value: string; - className?: string; - loading?: boolean; -}) { - return ( -
-
{label}
- {loading ? ( -
- ) : ( -
{value}
- )} -
- ); -} diff --git a/apps/webapp/app/components/run/RunTimeline.tsx b/apps/webapp/app/components/run/RunTimeline.tsx index edc576980c6..a6f024ac05f 100644 --- a/apps/webapp/app/components/run/RunTimeline.tsx +++ b/apps/webapp/app/components/run/RunTimeline.tsx @@ -26,7 +26,7 @@ export type TimelineEventVariant = | "end-cap"; // Timeline item type definitions -export type TimelineEventDefinition = { +type TimelineEventDefinition = { type: "event"; id: string; title: string; @@ -38,7 +38,7 @@ export type TimelineEventDefinition = { helpText?: string; }; -export type TimelineLineDefinition = { +type TimelineLineDefinition = { type: "line"; id: string; title: React.ReactNode; @@ -581,8 +581,6 @@ export type SpanTimelineProps = { style?: TimelineStyle; }; -export type SpanTimelineState = "error" | "pending" | "complete"; - export function SpanTimeline({ startTime, duration, diff --git a/apps/webapp/app/components/runs/v3/BatchFilters.tsx b/apps/webapp/app/components/runs/v3/BatchFilters.tsx index 3dff30204d7..eaca1d7f401 100644 --- a/apps/webapp/app/components/runs/v3/BatchFilters.tsx +++ b/apps/webapp/app/components/runs/v3/BatchFilters.tsx @@ -40,7 +40,7 @@ import { TimeFilter, } from "./SharedFilters"; -export const BatchStatus = z.enum(allBatchStatuses); +const BatchStatus = z.enum(allBatchStatuses); export const BatchListFilters = z.object({ cursor: z.string().optional(), diff --git a/apps/webapp/app/components/runs/v3/BatchStatus.tsx b/apps/webapp/app/components/runs/v3/BatchStatus.tsx index 243c5eaac19..7a806f0c018 100644 --- a/apps/webapp/app/components/runs/v3/BatchStatus.tsx +++ b/apps/webapp/app/components/runs/v3/BatchStatus.tsx @@ -41,7 +41,7 @@ export function BatchStatusCombo({ ); } -export function BatchStatusLabel({ status }: { status: BatchTaskRunStatus }) { +function BatchStatusLabel({ status }: { status: BatchTaskRunStatus }) { // system-mono-label: System themes uncolor the label (see tailwind.css) return ( @@ -50,13 +50,7 @@ export function BatchStatusLabel({ status }: { status: BatchTaskRunStatus }) { ); } -export function BatchStatusIcon({ - status, - className, -}: { - status: BatchTaskRunStatus; - className: string; -}) { +function BatchStatusIcon({ status, className }: { status: BatchTaskRunStatus; className: string }) { switch (status) { case "PROCESSING": return ; @@ -74,7 +68,7 @@ export function BatchStatusIcon({ } } -export function batchStatusColor(status: BatchTaskRunStatus): string { +function batchStatusColor(status: BatchTaskRunStatus): string { switch (status) { case "PROCESSING": return "text-blue-500"; diff --git a/apps/webapp/app/components/runs/v3/BulkAction.tsx b/apps/webapp/app/components/runs/v3/BulkAction.tsx index c472ebdaaef..3b8832888ad 100644 --- a/apps/webapp/app/components/runs/v3/BulkAction.tsx +++ b/apps/webapp/app/components/runs/v3/BulkAction.tsx @@ -23,11 +23,11 @@ export function BulkActionTypeCombo({ ); } -export function BulkActionLabel({ type, className }: { type: BulkActionType; className?: string }) { +function BulkActionLabel({ type, className }: { type: BulkActionType; className?: string }) { return {bulkActionTitle(type)}; } -export function BulkActionIcon({ type, className }: { type: BulkActionType; className: string }) { +function BulkActionIcon({ type, className }: { type: BulkActionType; className: string }) { switch (type) { case "REPLAY": return ; @@ -39,7 +39,7 @@ export function BulkActionIcon({ type, className }: { type: BulkActionType; clas } } -export function bulkActionClassName(type: BulkActionType): string { +function bulkActionClassName(type: BulkActionType): string { switch (type) { case "REPLAY": return "text-indigo-500"; @@ -51,7 +51,7 @@ export function bulkActionClassName(type: BulkActionType): string { } } -export function bulkActionTitle(type: BulkActionType): string { +function bulkActionTitle(type: BulkActionType): string { switch (type) { case "REPLAY": return "Replay"; @@ -63,18 +63,6 @@ export function bulkActionTitle(type: BulkActionType): string { } } -export function bulkActionVerb(type: BulkActionType): string { - switch (type) { - case "REPLAY": - return "Replaying"; - case "CANCEL": - return "Canceling"; - default: { - assertNever(type); - } - } -} - export function BulkActionStatusCombo({ status, className, @@ -94,7 +82,7 @@ export function BulkActionStatusCombo({ ); } -export function BulkActionStatusIcon({ +function BulkActionStatusIcon({ status, className, }: { @@ -114,7 +102,7 @@ export function BulkActionStatusIcon({ } } -export function BulkActionStatusLabel({ +function BulkActionStatusLabel({ status, className, }: { diff --git a/apps/webapp/app/components/runs/v3/CheckBatchCompletionDialog.tsx b/apps/webapp/app/components/runs/v3/CheckBatchCompletionDialog.tsx deleted file mode 100644 index 1f5f07bbf72..00000000000 --- a/apps/webapp/app/components/runs/v3/CheckBatchCompletionDialog.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { DialogClose } from "@radix-ui/react-dialog"; -import { Form, useNavigation } from "@remix-run/react"; -import { Button } from "~/components/primitives/Buttons"; -import { DialogContent, DialogHeader } from "~/components/primitives/Dialog"; -import { FormButtons } from "~/components/primitives/FormButtons"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import { SpinnerWhite } from "~/components/primitives/Spinner"; - -type CheckBatchCompletionDialogProps = { - batchId: string; - redirectPath: string; -}; - -export function CheckBatchCompletionDialog({ - batchId, - redirectPath, -}: CheckBatchCompletionDialogProps) { - const navigation = useNavigation(); - - const formAction = `/resources/batches/${batchId}/check-completion`; - const isLoading = navigation.formAction === formAction; - - return ( - - Try and resume batch -
- - In rare cases, parent runs don't continue after child runs have completed. - - - If this doesn't help, please get in touch. We are working on a permanent fix for this. - - - - - } - cancelButton={ - - - - } - /> -
-
- ); -} diff --git a/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx b/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx index aae5f97ecb4..e72628bad4c 100644 --- a/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx +++ b/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx @@ -27,7 +27,7 @@ export function DeploymentStatus({ ); } -export function DeploymentStatusLabel({ +function DeploymentStatusLabel({ status, isBuilt, }: { @@ -42,7 +42,7 @@ export function DeploymentStatusLabel({ ); } -export function DeploymentStatusIcon({ +function DeploymentStatusIcon({ status, className, }: { @@ -76,7 +76,7 @@ export function DeploymentStatusIcon({ } } -export function deploymentStatusClassNameColor(status: WorkerDeploymentStatus): string { +function deploymentStatusClassNameColor(status: WorkerDeploymentStatus): string { switch (status) { case "PENDING": return "text-text-faint"; @@ -97,7 +97,7 @@ export function deploymentStatusClassNameColor(status: WorkerDeploymentStatus): } } -export function deploymentStatusTitle(status: WorkerDeploymentStatus, isBuilt: boolean): string { +function deploymentStatusTitle(status: WorkerDeploymentStatus, isBuilt: boolean): string { switch (status) { case "PENDING": return "Queued…"; diff --git a/apps/webapp/app/components/runs/v3/LiveTimer.tsx b/apps/webapp/app/components/runs/v3/LiveTimer.tsx index 953bfb320b4..3128c6003cc 100644 --- a/apps/webapp/app/components/runs/v3/LiveTimer.tsx +++ b/apps/webapp/app/components/runs/v3/LiveTimer.tsx @@ -36,37 +36,6 @@ export function LiveTimer({ ); } -export function LiveCountUp({ - lastUpdated, - updateInterval = 250, - className, -}: { - lastUpdated: Date; - updateInterval?: number; - className?: string; -}) { - const [now, setNow] = useState(); - - useEffect(() => { - const interval = setInterval(() => { - const date = new Date(); - setNow(date); - }, updateInterval); - - return () => clearInterval(interval); - }, [lastUpdated]); - - return ( - <> - {formatDuration(lastUpdated, now, { - style: "short", - maxDecimalPoints: 0, - units: ["m", "s"], - })} - - ); -} - export function LiveCountdown({ endTime, updateInterval = 100, diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 560ce0fea39..7dd6e7d9a61 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -87,7 +87,7 @@ import { } from "./TaskRunStatus"; import { TaskTriggerSourceIcon } from "./TaskTriggerSource"; -export const RunStatus = z.enum(allTaskRunStatuses); +const RunStatus = z.enum(allTaskRunStatuses); const StringOrStringArray = z.preprocess((value) => { if (typeof value === "string") { @@ -105,7 +105,7 @@ const StringOrStringArray = z.preprocess((value) => { return undefined; }, z.string().array().optional()); -export const MachinePresetOrMachinePresetArray = z.preprocess((value) => { +const MachinePresetOrMachinePresetArray = z.preprocess((value) => { if (typeof value === "string") { if (value.length > 0) { const parsed = MachinePresetName.safeParse(value); diff --git a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx index de1c6b3ad7a..ce8f9c5f001 100644 --- a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx +++ b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx @@ -1,22 +1,4 @@ -import * as Ariakit from "@ariakit/react"; -import { ClockIcon, XMarkIcon } from "@heroicons/react/20/solid"; -import { useNavigate } from "@remix-run/react"; -import { useCallback, useRef } from "react"; import { z } from "zod"; -import { AppliedFilter } from "~/components/primitives/AppliedFilter"; -import { SearchInput } from "~/components/primitives/SearchInput"; -import { - SelectItem, - SelectList, - SelectPopover, - SelectProvider, -} from "~/components/primitives/Select"; -import { ShortcutKey } from "~/components/primitives/ShortcutKey"; -import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; -import { useShortcutKeys } from "~/hooks/useShortcutKeys"; -import { Button } from "../../primitives/Buttons"; -import { ScheduleTypeIcon, scheduleTypeName } from "./ScheduleType"; -import { FilterMenuProvider } from "./SharedFilters"; export const ScheduleListFilters = z.object({ page: z.coerce.number().default(1), @@ -29,233 +11,3 @@ export const ScheduleListFilters = z.object({ }); export type ScheduleListFilters = z.infer; - -type ScheduleFiltersProps = { - possibleTasks: string[]; -}; - -export function ScheduleFilters({ possibleTasks }: ScheduleFiltersProps) { - const location = useOptimisticLocation(); - const searchParams = new URLSearchParams(location.search); - const hasFilters = - searchParams.has("tasks") || searchParams.has("search") || searchParams.has("type"); - - return ( -
- - - - {hasFilters && } -
- ); -} - -function ScheduleSearchInput() { - return ; -} - -const typeShortcut = { key: "y" }; - -function PermanentTypeFilter() { - const navigate = useNavigate(); - const location = useOptimisticLocation(); - const searchParams = new URLSearchParams(location.search); - const currentType = searchParams.get("type") ?? undefined; - const triggerRef = useRef(null); - - useShortcutKeys({ - shortcut: typeShortcut, - action: (e) => { - e.preventDefault(); - e.stopPropagation(); - triggerRef.current?.click(); - }, - }); - - const handleChange = useCallback( - (value: string | string[]) => { - const selected = Array.isArray(value) ? value[0] : value; - const params = new URLSearchParams(location.search); - if (!selected || selected === "ALL") { - params.delete("type"); - } else { - params.set("type", selected); - } - params.delete("page"); - navigate(`${location.pathname}?${params.toString()}`); - }, - [location, navigate] - ); - - const typeLabel = currentType - ? scheduleTypeName(currentType.toUpperCase() as "IMPERATIVE" | "DECLARATIVE") - : "All types"; - - return ( - - {() => ( - - - } - /> - } - > - handleChange("ALL")} - variant="secondary/small" - /> - - -
- Filter by type - -
-
-
- - - - All types - - -
- - {scheduleTypeName("DECLARATIVE")} -
-
- -
- - {scheduleTypeName("IMPERATIVE")} -
-
-
-
-
- )} -
- ); -} - -const taskShortcut = { key: "t" }; - -function PermanentTaskFilter({ possibleTasks }: { possibleTasks: string[] }) { - const navigate = useNavigate(); - const location = useOptimisticLocation(); - const searchParams = new URLSearchParams(location.search); - const currentTask = searchParams.get("tasks") ?? undefined; - const triggerRef = useRef(null); - - useShortcutKeys({ - shortcut: taskShortcut, - action: (e) => { - e.preventDefault(); - e.stopPropagation(); - triggerRef.current?.click(); - }, - }); - - const handleChange = useCallback( - (value: string | string[]) => { - const selected = Array.isArray(value) ? value[0] : value; - const params = new URLSearchParams(location.search); - if (!selected || selected === "ALL") { - params.delete("tasks"); - } else { - params.set("tasks", selected); - } - params.delete("page"); - navigate(`${location.pathname}?${params.toString()}`); - }, - [location, navigate] - ); - - const taskLabel = currentTask ?? "All tasks"; - - return ( - - {() => ( - - - } - /> - } - > - } - value={taskLabel} - removable={!!currentTask} - onRemove={() => handleChange("ALL")} - variant="secondary/small" - /> - - -
- Filter by task - -
-
-
- - - - All tasks - - {possibleTasks.map((task) => ( - } - className="text-text-bright" - > - {task} - - ))} - - -
- )} -
- ); -} - -function ClearFiltersButton() { - const navigate = useNavigate(); - const location = useOptimisticLocation(); - - const clearFilters = useCallback(() => { - const params = new URLSearchParams(location.search); - params.delete("page"); - params.delete("tasks"); - params.delete("search"); - params.delete("type"); - navigate(`${location.pathname}?${params.toString()}`); - }, [location, navigate]); - - return ( -
-
- ); -} diff --git a/apps/webapp/app/components/runs/v3/SharedFilters.tsx b/apps/webapp/app/components/runs/v3/SharedFilters.tsx index f87d1031bae..756af300ff9 100644 --- a/apps/webapp/app/components/runs/v3/SharedFilters.tsx +++ b/apps/webapp/app/components/runs/v3/SharedFilters.tsx @@ -464,7 +464,7 @@ function getInitialCustomDuration(period?: string): { value: string; unit: strin type SectionType = "duration" | "dateRange"; -export function TimeDropdown({ +function TimeDropdown({ trigger, period, from, diff --git a/apps/webapp/app/components/runs/v3/SpanEvents.tsx b/apps/webapp/app/components/runs/v3/SpanEvents.tsx index 069246c89b7..9a06b0b2058 100644 --- a/apps/webapp/app/components/runs/v3/SpanEvents.tsx +++ b/apps/webapp/app/components/runs/v3/SpanEvents.tsx @@ -67,7 +67,7 @@ function SpanEvent({ spanEvent }: { spanEvent: OtelSpanEvent }) { ); } -export function SpanEventError({ +function SpanEventError({ spanEvent, exception, }: { diff --git a/apps/webapp/app/components/runs/v3/SpanTitle.tsx b/apps/webapp/app/components/runs/v3/SpanTitle.tsx index be363ca6ab3..eb08937de86 100644 --- a/apps/webapp/app/components/runs/v3/SpanTitle.tsx +++ b/apps/webapp/app/components/runs/v3/SpanTitle.tsx @@ -103,7 +103,7 @@ function SpanPill({ text, icon }: { text: string; icon?: string }) { ); } -export function SpanCodePathAccessory({ +function SpanCodePathAccessory({ accessory, className, }: { diff --git a/apps/webapp/app/components/runs/v3/TaskPath.tsx b/apps/webapp/app/components/runs/v3/TaskPath.tsx index 2ccb01c3688..fbf2410302c 100644 --- a/apps/webapp/app/components/runs/v3/TaskPath.tsx +++ b/apps/webapp/app/components/runs/v3/TaskPath.tsx @@ -1,25 +1,7 @@ import type { InlineCodeVariant } from "~/components/code/InlineCode"; import { InlineCode } from "~/components/code/InlineCode"; -import { SpanCodePathAccessory } from "./SpanTitle"; import { cn } from "~/utils/cn"; -type TaskPathProps = { - filePath: string; - functionName: string; - className?: string; -}; - -export function TaskPath({ filePath, functionName, className }: TaskPathProps) { - return ( - - ); -} - type TaskFileNameProps = { fileName: string; variant?: InlineCodeVariant; diff --git a/apps/webapp/app/components/runs/v3/TaskRunAttemptStatus.tsx b/apps/webapp/app/components/runs/v3/TaskRunAttemptStatus.tsx index 6358ccecd9a..6c8bebf71d1 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunAttemptStatus.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunAttemptStatus.tsx @@ -9,13 +9,8 @@ import type { TaskRunAttemptStatus as TaskRunAttemptStatusType } from "@trigger. import assertNever from "assert-never"; import { HourglassIcon } from "lucide-react"; import { Spinner } from "~/components/primitives/Spinner"; -import { TaskRunAttemptStatus } from "~/database-types"; import { cn } from "~/utils/cn"; -export const allTaskRunAttemptStatuses = Object.values( - TaskRunAttemptStatus -) as TaskRunAttemptStatusType[]; - export type ExtendedTaskAttemptStatus = TaskRunAttemptStatusType | "ENQUEUED"; export function TaskRunAttemptStatusCombo({ @@ -33,11 +28,7 @@ export function TaskRunAttemptStatusCombo({ ); } -export function TaskRunAttemptStatusLabel({ - status, -}: { - status: ExtendedTaskAttemptStatus | null; -}) { +function TaskRunAttemptStatusLabel({ status }: { status: ExtendedTaskAttemptStatus | null }) { return ( // system-mono-label: System themes uncolor the label (see tailwind.css) @@ -46,7 +37,7 @@ export function TaskRunAttemptStatusLabel({ ); } -export function TaskRunAttemptStatusIcon({ +function TaskRunAttemptStatusIcon({ status, className, }: { @@ -80,7 +71,7 @@ export function TaskRunAttemptStatusIcon({ } } -export function runAttemptStatusClassNameColor(status: ExtendedTaskAttemptStatus | null): string { +function runAttemptStatusClassNameColor(status: ExtendedTaskAttemptStatus | null): string { if (status === null) { return "text-text-faint"; } @@ -106,7 +97,7 @@ export function runAttemptStatusClassNameColor(status: ExtendedTaskAttemptStatus } } -export function runAttemptStatusTitle(status: ExtendedTaskAttemptStatus | null): string { +function runAttemptStatusTitle(status: ExtendedTaskAttemptStatus | null): string { if (status === null) { return "Enqueued"; } diff --git a/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx b/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx index d0a4c74ffbb..48e5821b39a 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx @@ -16,7 +16,6 @@ import { runFriendlyStatus, type RunFriendlyStatus } from "@trigger.dev/core/v3" import assertNever from "assert-never"; import { HourglassIcon } from "lucide-react"; import { TimedOutIcon } from "~/assets/icons/TimedOutIcon"; -import { Callout } from "~/components/primitives/Callout"; import { Spinner } from "~/components/primitives/Spinner"; import { cn } from "~/utils/cn"; @@ -111,44 +110,7 @@ export function TaskRunStatusCombo({ ); } -const statusReasonsToDescription: Record = { - NO_DEPLOYMENT: "No deployment or deployment image reference found for deployed run", - NO_WORKER: "No worker found for run", - TASK_NEVER_REGISTERED: "Task never registered", - QUEUE_NOT_FOUND: "Queue not found", - TASK_NOT_IN_LATEST: "Task not in latest version", - BACKGROUND_WORKER_MISMATCH: "Background worker mismatch", -}; - -export function TaskRunStatusReason({ - status, - statusReason, -}: { - status: TaskRunStatus; - statusReason?: string; -}) { - if (status !== "PENDING_VERSION") { - return null; - } - - if (!statusReason) { - return null; - } - - const description = statusReasonsToDescription[statusReason]; - - if (!description) { - return null; - } - - return ( - - {description} - - ); -} - -export function TaskRunStatusLabel({ status }: { status: TaskRunStatus }) { +function TaskRunStatusLabel({ status }: { status: TaskRunStatus }) { // system-mono-label: System themes uncolor the label (see tailwind.css) return ( diff --git a/apps/webapp/app/components/runs/v3/WaitpointStatus.tsx b/apps/webapp/app/components/runs/v3/WaitpointStatus.tsx index e5825088b3a..6ee3adc2f0d 100644 --- a/apps/webapp/app/components/runs/v3/WaitpointStatus.tsx +++ b/apps/webapp/app/components/runs/v3/WaitpointStatus.tsx @@ -22,7 +22,7 @@ export function WaitpointStatusCombo({ ); } -export function WaitpointStatusLabel({ status }: { status: WaitpointTokenStatus }) { +function WaitpointStatusLabel({ status }: { status: WaitpointTokenStatus }) { return ( // system-mono-label: System themes uncolor the label (see tailwind.css) @@ -31,7 +31,7 @@ export function WaitpointStatusLabel({ status }: { status: WaitpointTokenStatus ); } -export function WaitpointStatusIcon({ +function WaitpointStatusIcon({ status, className, }: { @@ -51,7 +51,7 @@ export function WaitpointStatusIcon({ } } -export function waitpointStatusClassNameColor(status: WaitpointTokenStatus): string { +function waitpointStatusClassNameColor(status: WaitpointTokenStatus): string { switch (status) { case "WAITING": return "text-blue-500"; diff --git a/apps/webapp/app/components/runs/v3/ai/index.ts b/apps/webapp/app/components/runs/v3/ai/index.ts index 5acb9ff17a2..14bc84fb114 100644 --- a/apps/webapp/app/components/runs/v3/ai/index.ts +++ b/apps/webapp/app/components/runs/v3/ai/index.ts @@ -2,7 +2,4 @@ export { AISpanDetails } from "./AISpanDetails"; export { extractAISpanData } from "./extractAISpanData"; export { extractAISummarySpanData } from "./extractAISummarySpanData"; export { AIToolCallSpanDetails, extractAIToolCallData } from "./AIToolCallSpanDetails"; -export type { AIToolCallData } from "./AIToolCallSpanDetails"; export { AIEmbedSpanDetails, extractAIEmbedData } from "./AIEmbedSpanDetails"; -export type { AIEmbedData } from "./AIEmbedSpanDetails"; -export type { AISpanData, DisplayItem, ToolUse } from "./types"; diff --git a/apps/webapp/app/components/runs/v3/ai/types.ts b/apps/webapp/app/components/runs/v3/ai/types.ts index b1765a2e59c..adb085714af 100644 --- a/apps/webapp/app/components/runs/v3/ai/types.ts +++ b/apps/webapp/app/components/runs/v3/ai/types.ts @@ -34,25 +34,25 @@ export type ToolUse = { // --------------------------------------------------------------------------- /** System prompt text (collapsible) */ -export type SystemItem = { +type SystemItem = { type: "system"; text: string; }; /** User message text */ -export type UserItem = { +type UserItem = { type: "user"; text: string; }; /** One or more tool calls with their results, grouped */ -export type ToolUseItem = { +type ToolUseItem = { type: "tool-use"; tools: ToolUse[]; }; /** Final assistant text response */ -export type AssistantItem = { +type AssistantItem = { type: "assistant"; text: string; }; diff --git a/apps/webapp/app/components/schedules/ScheduleInspector.tsx b/apps/webapp/app/components/schedules/ScheduleInspector.tsx index b2a64450f11..5b65b73afb7 100644 --- a/apps/webapp/app/components/schedules/ScheduleInspector.tsx +++ b/apps/webapp/app/components/schedules/ScheduleInspector.tsx @@ -47,7 +47,7 @@ type EnvironmentRow = React.ComponentProps["environment id: string; }; -export type ScheduleInspectorData = { +type ScheduleInspectorData = { id: string; friendlyId: string; type: "DECLARATIVE" | "IMPERATIVE"; diff --git a/apps/webapp/app/components/sessions/v1/SessionFilters.tsx b/apps/webapp/app/components/sessions/v1/SessionFilters.tsx index 57ff6526df3..315b9b7d482 100644 --- a/apps/webapp/app/components/sessions/v1/SessionFilters.tsx +++ b/apps/webapp/app/components/sessions/v1/SessionFilters.tsx @@ -52,7 +52,7 @@ const StringOrStringArray = z.preprocess( z.array(z.string()).optional() ); -export const SessionStatus = z.enum(allSessionStatuses); +const SessionStatus = z.enum(allSessionStatuses); export const SessionListSearchFilters = z.object({ cursor: z.string().optional(), @@ -71,7 +71,6 @@ export const SessionListSearchFilters = z.object({ }); export type SessionListSearchFilters = z.infer; -export type SessionListSearchFilterKey = keyof SessionListSearchFilters; export function getSessionFiltersFromSearchParams( searchParams: URLSearchParams diff --git a/apps/webapp/app/components/sessions/v1/SessionStatus.tsx b/apps/webapp/app/components/sessions/v1/SessionStatus.tsx index 69dfdf5092d..681baa8c54a 100644 --- a/apps/webapp/app/components/sessions/v1/SessionStatus.tsx +++ b/apps/webapp/app/components/sessions/v1/SessionStatus.tsx @@ -30,7 +30,7 @@ export function sessionStatusTitle(status: SessionStatus): string { } } -export function sessionStatusColor(status: SessionStatus): string { +function sessionStatusColor(status: SessionStatus): string { switch (status) { case "ACTIVE": return "text-pending"; @@ -43,7 +43,7 @@ export function sessionStatusColor(status: SessionStatus): string { } } -export function SessionStatusIcon({ +function SessionStatusIcon({ status, className, pulse = true, @@ -73,7 +73,7 @@ export function SessionStatusIcon({ } } -export function SessionStatusLabel({ status }: { status: SessionStatus }) { +function SessionStatusLabel({ status }: { status: SessionStatus }) { // system-mono-label: System themes uncolor the label (see tailwind.css) return ( diff --git a/apps/webapp/app/components/webhookConsole/WebhookComposer.tsx b/apps/webapp/app/components/webhookConsole/WebhookComposer.tsx index cee5b70fe96..9c9d76038cd 100644 --- a/apps/webapp/app/components/webhookConsole/WebhookComposer.tsx +++ b/apps/webapp/app/components/webhookConsole/WebhookComposer.tsx @@ -26,7 +26,7 @@ import { SampleSourcePicker } from "./SampleSourcePicker"; type SourceTab = "body" | "sample" | "replay" | "ai"; -export type WebhookComposerEndpoint = { +type WebhookComposerEndpoint = { friendlyId: string; label: string; source: string; diff --git a/apps/webapp/app/components/webhookDeliveries/v1/DeliveryStatus.tsx b/apps/webapp/app/components/webhookDeliveries/v1/DeliveryStatus.tsx index 0fac38937a4..8558e1bdaf9 100644 --- a/apps/webapp/app/components/webhookDeliveries/v1/DeliveryStatus.tsx +++ b/apps/webapp/app/components/webhookDeliveries/v1/DeliveryStatus.tsx @@ -3,7 +3,7 @@ import { cn } from "~/utils/cn"; // Reuse the run-status hex palette for the four delivery statuses (matches the // detail page activity chart and the task-list status bars). No invented colors. -export const DELIVERY_STATUS_COLOR: Record = { +const DELIVERY_STATUS_COLOR: Record = { SUCCEEDED: "#28BF5C", FAILED: "#E11D48", PROCESSING: "#3B82F6", @@ -11,7 +11,7 @@ export const DELIVERY_STATUS_COLOR: Record = { FILTERED: "#64748B", // received + verified, intentionally not routed; neutral, not a failure }; -export const DELIVERY_STATUS_LABEL: Record = { +const DELIVERY_STATUS_LABEL: Record = { SUCCEEDED: "Succeeded", FAILED: "Failed", PROCESSING: "Processing", diff --git a/apps/webapp/app/components/webhookDeliveries/v1/buildDeliveryTimelineItems.ts b/apps/webapp/app/components/webhookDeliveries/v1/buildDeliveryTimelineItems.ts index f5cc0af596a..25ab202f9d8 100644 --- a/apps/webapp/app/components/webhookDeliveries/v1/buildDeliveryTimelineItems.ts +++ b/apps/webapp/app/components/webhookDeliveries/v1/buildDeliveryTimelineItems.ts @@ -5,7 +5,7 @@ import { type TimelineLineVariant, } from "~/components/run/RunTimeline"; -export type DeliveryRunTarget = { +type DeliveryRunTarget = { run: { friendlyId: string } | null; session: { friendlyId: string; externalId: string | null } | null; }; @@ -22,7 +22,7 @@ export type DeliveryTimelineEventItem = { target?: DeliveryRunTarget; }; -export type DeliveryTimelineLineItem = { +type DeliveryTimelineLineItem = { type: "line"; id: string; from: Date; diff --git a/apps/webapp/app/consts.ts b/apps/webapp/app/consts.ts index 4bd070a5baf..ba5fe8e065e 100644 --- a/apps/webapp/app/consts.ts +++ b/apps/webapp/app/consts.ts @@ -1,16 +1,6 @@ -export const LIVE_ENVIRONMENT = "live"; -export const DEV_ENVIRONMENT = "development"; -export const MAX_LIVE_PROJECTS = 1; -export const DEFAULT_MAX_CONCURRENT_RUNS = 10; -export const MAX_CONCURRENT_RUNS_LIMIT = 20; -export const PREPROCESS_RETRY_LIMIT = 2; -export const EXECUTE_JOB_RETRY_LIMIT = 10; -export const MAX_RUN_YIELDED_EXECUTIONS = 100; -export const RUN_CHUNK_EXECUTION_BUFFER = 350; -export const MAX_RUN_CHUNK_EXECUTION_LIMIT = 120000; // 2 minutes -export const VERCEL_RESPONSE_TIMEOUT_STATUS_CODES = [408, 504]; +// 2 minutes + export const MAX_BATCH_TRIGGER_ITEMS = 100; export const MAX_API_KEY_TASK_IDENTIFIERS = 10; -export const MAX_TASK_RUN_ATTEMPTS = 250; + export const BULK_ACTION_RUN_LIMIT = 250; -export const MAX_JOB_RUN_EXECUTION_COUNT = 250; diff --git a/apps/webapp/app/database-types.ts b/apps/webapp/app/database-types.ts index 3305dc67d57..f4bdf9fd654 100644 --- a/apps/webapp/app/database-types.ts +++ b/apps/webapp/app/database-types.ts @@ -52,11 +52,3 @@ export const RuntimeEnvironmentType = { DEVELOPMENT: "DEVELOPMENT", PREVIEW: "PREVIEW", } as const satisfies Record; - -export function isTaskRunAttemptStatus(value: string): value is keyof typeof TaskRunAttemptStatus { - return Object.values(TaskRunAttemptStatus).includes(value as keyof typeof TaskRunAttemptStatus); -} - -export function isTaskRunStatus(value: string): value is keyof typeof TaskRunStatus { - return Object.values(TaskRunStatus).includes(value as keyof typeof TaskRunStatus); -} diff --git a/apps/webapp/app/db.server.ts b/apps/webapp/app/db.server.ts index 077730cc33e..a69c83cd375 100644 --- a/apps/webapp/app/db.server.ts +++ b/apps/webapp/app/db.server.ts @@ -15,7 +15,6 @@ import { markReadReplicaClient } from "@internal/run-store"; import { PrismaPg } from "@prisma/adapter-pg"; import { Pool } from "pg"; import invariant from "tiny-invariant"; -import { z } from "zod"; import { env } from "./env.server"; import { logger } from "./services/logger.server"; import { isValidDatabaseUrl } from "./utils/db"; @@ -274,8 +273,8 @@ export const webhookReplica: WebhookReplicaDatabase = singleton("webhookReplica" return $replica; }); -export type RunOpsClients = { writer: PrismaClient; replica: PrismaReplicaClient }; -export type NewRunOpsClients = { writer: RunOpsPrismaClient; replica: RunOpsPrismaClient }; +type RunOpsClients = { writer: PrismaClient; replica: PrismaReplicaClient }; +type NewRunOpsClients = { writer: RunOpsPrismaClient; replica: RunOpsPrismaClient }; export type RunOpsTopology = { newRunOps: NewRunOpsClients; legacyRunOps: RunOpsClients; @@ -1142,10 +1141,6 @@ function redactUrlSecrets(hrefOrUrl: string | URL) { export type { PrismaClient } from "@trigger.dev/database"; -export const PrismaErrorSchema = z.object({ - code: z.string(), -}); - function getDatabaseSchema() { if (!isValidDatabaseUrl(env.DATABASE_URL)) { throw new Error("Invalid Database URL"); @@ -1162,6 +1157,6 @@ function getDatabaseSchema() { return schemaFromSearchParam; } -export const DATABASE_SCHEMA = singleton("DATABASE_SCHEMA", getDatabaseSchema); +const DATABASE_SCHEMA = singleton("DATABASE_SCHEMA", getDatabaseSchema); export const sqlDatabaseSchema = Prisma.sql([`${DATABASE_SCHEMA}`]); diff --git a/apps/webapp/app/hooks/useCanViewLogsPage.ts b/apps/webapp/app/hooks/useCanViewLogsPage.ts deleted file mode 100644 index 3eb36b0641b..00000000000 --- a/apps/webapp/app/hooks/useCanViewLogsPage.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useEffect } from "react"; -import { useTypedFetcher } from "remix-typedjson"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { type loader as canViewLogsPageLoader } from "~/routes/resources.orgs.$organizationSlug.can-view-logs-page/route"; - -export function useCanViewLogsPage(): boolean | undefined { - const organization = useOrganization(); - const fetcher = useTypedFetcher(); - - useEffect(() => { - const url = `/resources/orgs/${organization.slug}/can-view-logs-page`; - fetcher.load(url); - }, [organization.slug]); - - return fetcher.data?.canViewLogsPage; -} diff --git a/apps/webapp/app/hooks/useEnvironments.ts b/apps/webapp/app/hooks/useEnvironments.ts deleted file mode 100644 index 43fe8a7b8bc..00000000000 --- a/apps/webapp/app/hooks/useEnvironments.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { UIMatch } from "@remix-run/react"; -import type { MatchedProject } from "./useProject"; -import { useOptionalProject } from "./useProject"; - -export type ProjectJobEnvironment = MatchedProject["environments"][number]; - -export function useEnvironments(matches?: UIMatch[]) { - const project = useOptionalProject(matches); - if (!project) return; - - return project.environments; -} diff --git a/apps/webapp/app/hooks/useList.tsx b/apps/webapp/app/hooks/useList.tsx index 1d0c9dcfde8..687301f0551 100644 --- a/apps/webapp/app/hooks/useList.tsx +++ b/apps/webapp/app/hooks/useList.tsx @@ -1,7 +1,7 @@ import type { Reducer } from "react"; import { useReducer } from "react"; -export type ListState = { +type ListState = { items: T[]; }; diff --git a/apps/webapp/app/hooks/useOrganizations.ts b/apps/webapp/app/hooks/useOrganizations.ts index df3ec699633..4070976dafb 100644 --- a/apps/webapp/app/hooks/useOrganizations.ts +++ b/apps/webapp/app/hooks/useOrganizations.ts @@ -8,7 +8,7 @@ import { useTypedMatchesData } from "./useTypedMatchData"; export type MatchedOrganization = UseDataFunctionReturn["organizations"][number]; export const organizationMatchId = "routes/_app.orgs.$organizationSlug"; -export function useOptionalOrganizations(matches?: UIMatch[]) { +function useOptionalOrganizations(matches?: UIMatch[]) { const data = useTypedMatchesData({ id: "routes/_app.orgs.$organizationSlug", matches, @@ -42,14 +42,6 @@ export function useOrganization(matches?: UIMatch[]) { return org; } -export function useIsNewOrganizationPage(matches?: UIMatch[]): boolean { - const data = useTypedMatchesData({ - id: "routes/_app.orgs.new", - matches, - }); - return !!data; -} - export const useOrganizationChanged = (action: (org: MatchedOrganization | undefined) => void) => { useChanged(useOptionalOrganization, action); }; @@ -62,8 +54,6 @@ export function useIsImpersonating(matches?: UIMatch[]) { return data?.isImpersonating === true; } -export type CustomDashboard = UseDataFunctionReturn["customDashboards"][number]; - export function useCustomDashboards(matches?: UIMatch[]) { const data = useTypedMatchesData({ id: "routes/_app.orgs.$organizationSlug", diff --git a/apps/webapp/app/hooks/useRevalidateOnParam.ts b/apps/webapp/app/hooks/useRevalidateOnParam.ts deleted file mode 100644 index de05141d95e..00000000000 --- a/apps/webapp/app/hooks/useRevalidateOnParam.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useEffect } from "react"; -import { useRevalidator, useSearchParams } from "@remix-run/react"; - -type UseRevalidateOnParamOptions = { - /** The query param(s) that trigger revalidation */ - param: string | string[]; - /** Callback fired when revalidation is triggered */ - onRevalidate?: () => void; -}; - -/** - * Hook that triggers revalidation when specific query params are present, - * then removes those params from the URL. - * - * Usage: - * ```ts - * // Revalidate when ?_revalidate is present - * useRevalidateOnParam({ param: "_revalidate" }); - * - * // With callback to close a modal - * useRevalidateOnParam({ - * param: "_revalidate", - * onRevalidate: () => setEditorMode(null), - * }); - * ``` - * - * The redirect should include the param: - * ```ts - * return redirect(`${dashboardPath}?_revalidate=${Date.now()}`); - * ``` - */ -export function useRevalidateOnParam({ param, onRevalidate }: UseRevalidateOnParamOptions) { - const [searchParams, setSearchParams] = useSearchParams(); - const revalidator = useRevalidator(); - - const paramArray = Array.isArray(param) ? param : [param]; - - useEffect(() => { - // Check if any of the trigger params are present - const hasParam = paramArray.some((p) => searchParams.has(p)); - - if (hasParam) { - // Trigger revalidation - revalidator.revalidate(); - - // Call the callback if provided - onRevalidate?.(); - - // Remove the trigger params from the URL - const newParams = new URLSearchParams(searchParams); - paramArray.forEach((p) => newParams.delete(p)); - - // Update URL without the params (replace to avoid adding to history) - setSearchParams(newParams, { replace: true }); - } - }, [searchParams, setSearchParams, revalidator, paramArray, onRevalidate]); -} diff --git a/apps/webapp/app/hooks/useTextFilter.ts b/apps/webapp/app/hooks/useTextFilter.ts deleted file mode 100644 index b28019bd20d..00000000000 --- a/apps/webapp/app/hooks/useTextFilter.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useMemo, useState } from "react"; - -type TextFilterProps = { - defaultValue?: string; - items: T[]; - filter: (item: T, filterText: string) => boolean; -}; - -export function useTextFilter({ defaultValue = "", items, filter }: TextFilterProps) { - const [filterText, setFilterText] = useState(defaultValue); - - const filteredItems = useMemo(() => { - if (filterText === "") { - return items; - } - return items.filter((item) => { - return filter(item, filterText); - }); - }, [items, filterText]); - - return { - filterText, - setFilterText, - filteredItems, - }; -} diff --git a/apps/webapp/app/hooks/useThrottle.ts b/apps/webapp/app/hooks/useThrottle.ts deleted file mode 100644 index d00ef9b460e..00000000000 --- a/apps/webapp/app/hooks/useThrottle.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect, useRef } from "react"; - -export function useThrottle(fn: (...args: any[]) => void, duration: number) { - const timeout = useRef>(); - - // Clean up when the component is unmounted - useEffect(() => { - return () => { - if (timeout.current) clearTimeout(timeout.current); - }; - }, []); - - return (...args: Parameters) => { - if (timeout.current) { - clearTimeout(timeout.current); - } - - timeout.current = setTimeout(() => { - fn(...args); - timeout.current = undefined; - }, duration); - }; -} diff --git a/apps/webapp/app/hooks/useToggleFilter.ts b/apps/webapp/app/hooks/useToggleFilter.ts deleted file mode 100644 index 2e099a9f979..00000000000 --- a/apps/webapp/app/hooks/useToggleFilter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useMemo, useState } from "react"; - -type ToggleFilterProps = { - items: T[]; - filter: (item: T, isToggleActive: boolean) => boolean; - defaultValue?: boolean; -}; - -export function useToggleFilter({ items, filter, defaultValue = false }: ToggleFilterProps) { - const [isToggleActive, setToggleActive] = useState(defaultValue); - - const filteredItems = useMemo(() => { - return items.filter((item) => filter(item, isToggleActive)); - }, [items, isToggleActive]); - - return { - isToggleActive, - setToggleActive, - filteredItems, - }; -} diff --git a/apps/webapp/app/hooks/useTypedMatchData.ts b/apps/webapp/app/hooks/useTypedMatchData.ts index d2a1514c59a..6022fee7550 100644 --- a/apps/webapp/app/hooks/useTypedMatchData.ts +++ b/apps/webapp/app/hooks/useTypedMatchData.ts @@ -30,7 +30,7 @@ export function useTypedMatchesData({ return useTypedDataFromMatches({ id, matches }); } -export function useTypedMatchData( +function useTypedMatchData( match: UIMatch | undefined ): UseDataFunctionReturn | undefined { if (!match) { diff --git a/apps/webapp/app/models/member.server.ts b/apps/webapp/app/models/member.server.ts index c70a41ae85f..de105c4d3cf 100644 --- a/apps/webapp/app/models/member.server.ts +++ b/apps/webapp/app/models/member.server.ts @@ -13,7 +13,7 @@ import { ssoController } from "~/services/sso.server"; import { boundedIn } from "@trigger.dev/database"; export const INVITE_NOT_FOUND = "Invite not found"; -export const INVITE_BLOCKED_DIRECTORY_MANAGED = +const INVITE_BLOCKED_DIRECTORY_MANAGED = "Membership for this organization is managed by Directory Sync, so invites can't be accepted."; export const ENV_SETUP_INCOMPLETE = "You joined the organization, but we couldn't finish setting up your development environments. Please try accepting the invite again, or contact support if this persists."; diff --git a/apps/webapp/app/models/message.server.ts b/apps/webapp/app/models/message.server.ts index eec7316e095..3ff8b4e1a94 100644 --- a/apps/webapp/app/models/message.server.ts +++ b/apps/webapp/app/models/message.server.ts @@ -94,38 +94,6 @@ export function setErrorMessage(session: Session, message: string, options?: Toa } as ToastMessage); } -export async function setRequestErrorMessage( - request: Request, - message: string, - options?: ToastMessageOptions -) { - const session = await getSession(request.headers.get("cookie")); - - setErrorMessage(session, message, options); - - return session; -} - -export async function setRequestSuccessMessage( - request: Request, - message: string, - options?: ToastMessageOptions -) { - const session = await getSession(request.headers.get("cookie")); - - setSuccessMessage(session, message, options); - - return session; -} - -export async function setToastMessageCookie(session: Session) { - return { - "Set-Cookie": await commitSession(session, { - expires: new Date(Date.now() + ONE_YEAR), - }), - }; -} - export async function jsonWithSuccessMessage( data: any, request: Request, diff --git a/apps/webapp/app/models/projectAlert.server.ts b/apps/webapp/app/models/projectAlert.server.ts index dbcb672ad7d..95909dbde96 100644 --- a/apps/webapp/app/models/projectAlert.server.ts +++ b/apps/webapp/app/models/projectAlert.server.ts @@ -15,10 +15,6 @@ export const ProjectAlertEmailProperties = z.object({ export type ProjectAlertEmailProperties = z.infer; -export const DeleteProjectAlertChannel = z.object({ - id: z.string(), -}); - export const ProjectAlertSlackProperties = z.object({ channelId: z.string(), channelName: z.string(), diff --git a/apps/webapp/app/models/runtimeEnvironment.server.ts b/apps/webapp/app/models/runtimeEnvironment.server.ts index e7cf10f3e02..790576200ec 100644 --- a/apps/webapp/app/models/runtimeEnvironment.server.ts +++ b/apps/webapp/app/models/runtimeEnvironment.server.ts @@ -499,77 +499,6 @@ export async function findEnvironmentFromRun( }; } -export async function createNewSession( - environment: Pick, - ipAddress: string -) { - const session = await prisma.runtimeEnvironmentSession.create({ - data: { - environmentId: environment.id, - ipAddress, - }, - }); - - await prisma.runtimeEnvironment.update({ - where: { - id: environment.id, - }, - data: { - currentSessionId: session.id, - }, - }); - - return session; -} - -export async function disconnectSession(environmentId: string) { - const environment = await prisma.runtimeEnvironment.findFirst({ - where: { - id: environmentId, - }, - }); - - if (!environment || !environment.currentSessionId) { - return null; - } - - const session = await prisma.runtimeEnvironmentSession.update({ - where: { - id: environment.currentSessionId, - }, - data: { - disconnectedAt: new Date(), - }, - }); - - await prisma.runtimeEnvironment.update({ - where: { - id: environment.id, - }, - data: { - currentSessionId: null, - }, - }); - - return session; -} - -export async function findLatestSession( - environmentId: string, - client: PrismaClientOrTransaction = $replica -) { - const session = await client.runtimeEnvironmentSession.findFirst({ - where: { - environmentId, - }, - orderBy: { - createdAt: "desc", - }, - }); - - return session; -} - export type DisplayableInputEnvironment = Prisma.RuntimeEnvironmentGetPayload<{ select: { id: true; diff --git a/apps/webapp/app/models/task.server.ts b/apps/webapp/app/models/task.server.ts index 54dca73a01b..5aa9a5096c0 100644 --- a/apps/webapp/app/models/task.server.ts +++ b/apps/webapp/app/models/task.server.ts @@ -3,7 +3,6 @@ import type { PrismaClientOrTransaction } from "~/db.server"; import { sqlDatabaseSchema } from "~/db.server"; export { getTaskIdentifiers } from "~/services/taskIdentifierRegistry.server"; -export type { TaskIdentifierEntry } from "~/services/taskIdentifierCache.server"; /** * diff --git a/apps/webapp/app/models/taskQueue.server.ts b/apps/webapp/app/models/taskQueue.server.ts index 0e9f26450ef..c8d449f165b 100644 --- a/apps/webapp/app/models/taskQueue.server.ts +++ b/apps/webapp/app/models/taskQueue.server.ts @@ -1,61 +1,4 @@ -import { QueueManifest } from "@trigger.dev/core/v3/schemas"; -import type { TaskQueue } from "@trigger.dev/database"; -import { prisma } from "~/db.server"; - -export async function findQueueInEnvironment( - queueName: string, - environmentId: string, - backgroundWorkerTaskId?: string, - backgroundTask?: { queueConfig?: unknown } -): Promise { - const sanitizedQueueName = sanitizeQueueName(queueName); - - const queue = await prisma.taskQueue.findFirst({ - where: { - runtimeEnvironmentId: environmentId, - name: sanitizedQueueName, - }, - }); - - if (queue) { - return queue; - } - - const task = backgroundTask - ? backgroundTask - : backgroundWorkerTaskId - ? await prisma.backgroundWorkerTask.findFirst({ - where: { - id: backgroundWorkerTaskId, - }, - }) - : undefined; - - if (!task) { - return; - } - - const queueConfig = QueueManifest.safeParse(task.queueConfig); - - if (queueConfig.success) { - const taskQueueName = queueConfig.data.name - ? sanitizeQueueName(queueConfig.data.name) - : undefined; - - if (taskQueueName && taskQueueName !== sanitizedQueueName) { - const queue = await prisma.taskQueue.findFirst({ - where: { - runtimeEnvironmentId: environmentId, - name: taskQueueName, - }, - }); - - if (queue) { - return queue; - } - } - } -} +import type {} from "@trigger.dev/database"; // Only allow alphanumeric characters, underscores, hyphens, and slashes (and only the first 128 characters) export function sanitizeQueueName(queueName: string) { diff --git a/apps/webapp/app/models/user.server.ts b/apps/webapp/app/models/user.server.ts index b499d753b45..ffca2ee0adf 100644 --- a/apps/webapp/app/models/user.server.ts +++ b/apps/webapp/app/models/user.server.ts @@ -64,9 +64,7 @@ export async function findOrCreateUser(input: FindOrCreateUser): Promise { +async function findOrCreateMagicLinkUser({ email }: FindOrCreateMagicLink): Promise { assertEmailAllowed(email); const existingUser = await prisma.user.findFirst({ @@ -97,7 +95,7 @@ export async function findOrCreateMagicLinkUser({ }; } -export async function findOrCreateGithubUser({ +async function findOrCreateGithubUser({ email, authenticationProfile, authenticationExtraParams, @@ -187,7 +185,7 @@ export async function findOrCreateGithubUser({ }; } -export async function findOrCreateGoogleUser({ +async function findOrCreateGoogleUser({ email, authenticationProfile, authenticationExtraParams, @@ -375,10 +373,6 @@ export async function getUserById(id: User["id"]) { }; } -export async function getUserByEmail(email: User["email"]) { - return prisma.user.findUnique({ where: { email } }); -} - export function updateUser({ id, name, @@ -403,16 +397,3 @@ export function updateUser({ }, }); } - -export async function grantUserCloudAccess({ id, inviteCode }: { id: string; inviteCode: string }) { - return prisma.user.update({ - where: { id }, - data: { - invitationCode: { - connect: { - code: inviteCode, - }, - }, - }, - }); -} diff --git a/apps/webapp/app/models/vercelIntegration.server.ts b/apps/webapp/app/models/vercelIntegration.server.ts index 9365dc46de0..68d6ebb66b3 100644 --- a/apps/webapp/app/models/vercelIntegration.server.ts +++ b/apps/webapp/app/models/vercelIntegration.server.ts @@ -124,7 +124,7 @@ function isVercelApiErrorShape(error: unknown): error is VercelApiError { // Schemas & token types // --------------------------------------------------------------------------- -export const VercelSecretSchema = z.object({ +const VercelSecretSchema = z.object({ accessToken: z.string(), tokenType: z.string().optional(), teamId: z.string().nullable().optional(), @@ -133,7 +133,7 @@ export const VercelSecretSchema = z.object({ raw: z.record(z.any()).optional(), }); -export type VercelSecret = z.infer; +type VercelSecret = z.infer; export type TokenResponse = { accessToken: string; diff --git a/apps/webapp/app/models/vercelSdkRecovery.server.ts b/apps/webapp/app/models/vercelSdkRecovery.server.ts index d3e1bfd6961..4def25124cd 100644 --- a/apps/webapp/app/models/vercelSdkRecovery.server.ts +++ b/apps/webapp/app/models/vercelSdkRecovery.server.ts @@ -52,7 +52,7 @@ function extractRawValue(error: unknown): unknown | undefined { * * Returns the validated data on success, or `undefined` if recovery fails. */ -export function recoverFromVercelSdkError( +function recoverFromVercelSdkError( error: unknown, schema: z.ZodType, options?: { context?: string } diff --git a/apps/webapp/app/presenters/ProjectPresenter.server.ts b/apps/webapp/app/presenters/ProjectPresenter.server.ts deleted file mode 100644 index 773b78edab0..00000000000 --- a/apps/webapp/app/presenters/ProjectPresenter.server.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { PrismaClient } from "~/db.server"; -import { prisma } from "~/db.server"; -import type { Project } from "~/models/project.server"; -import { displayableEnvironment } from "~/models/runtimeEnvironment.server"; -import type { User } from "~/models/user.server"; -import { sortEnvironments } from "~/utils/environmentSort"; - -export class ProjectPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - id, - }: Pick & { - userId: User["id"]; - }) { - const project = await this.#prismaClient.project.findFirst({ - select: { - id: true, - slug: true, - name: true, - organizationId: true, - createdAt: true, - updatedAt: true, - deletedAt: true, - version: true, - externalRef: true, - environments: { - where: { archivedAt: null }, - select: { - id: true, - slug: true, - type: true, - orgMember: { - select: { - user: { - select: { - id: true, - name: true, - displayName: true, - }, - }, - }, - }, - apiKey: true, - }, - }, - }, - where: { id, deletedAt: null, organization: { members: { some: { userId } } } }, - }); - - if (!project) { - return undefined; - } - - return { - id: project.id, - slug: project.slug, - ref: project.externalRef, - name: project.name, - organizationId: project.organizationId, - createdAt: project.createdAt, - updatedAt: project.updatedAt, - deletedAt: project.deletedAt, - version: project.version, - environments: sortEnvironments( - project.environments.map((environment) => ({ - ...displayableEnvironment(environment, userId), - userId: environment.orgMember?.user.id, - })) - ), - }; - } -} diff --git a/apps/webapp/app/presenters/v3/AgentDetailPresenter.server.ts b/apps/webapp/app/presenters/v3/AgentDetailPresenter.server.ts index 5d4fa6914f8..78ca42b8eac 100644 --- a/apps/webapp/app/presenters/v3/AgentDetailPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/AgentDetailPresenter.server.ts @@ -18,7 +18,7 @@ export type AgentDetail = { config: unknown; }; -export type AgentActivityPoint = { +type AgentActivityPoint = { bucket: number; // epoch ms } & Record; diff --git a/apps/webapp/app/presenters/v3/AgentListPresenter.server.ts b/apps/webapp/app/presenters/v3/AgentListPresenter.server.ts index f866892469b..c97d7ca92df 100644 --- a/apps/webapp/app/presenters/v3/AgentListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/AgentListPresenter.server.ts @@ -1,8 +1,4 @@ -import { - type PrismaClientOrTransaction, - type RuntimeEnvironmentType, - type TaskTriggerSource, -} from "@trigger.dev/database"; +import { type PrismaClientOrTransaction, type RuntimeEnvironmentType } from "@trigger.dev/database"; import { type ClickHouse } from "@internal/clickhouse"; import { z } from "zod"; import { $replica } from "~/db.server"; @@ -10,20 +6,12 @@ import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstan import { singleton } from "~/utils/singleton"; import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server"; -export type AgentListItem = { - slug: string; - filePath: string; - createdAt: Date; - triggerSource: TaskTriggerSource; - config: unknown; -}; - export type AgentActiveState = { running: number; suspended: number; }; -export class AgentListPresenter { +class AgentListPresenter { constructor(private readonly _replica: PrismaClientOrTransaction) {} public async call({ diff --git a/apps/webapp/app/presenters/v3/AlertChannelListPresenter.server.ts b/apps/webapp/app/presenters/v3/AlertChannelListPresenter.server.ts index 0b1f6eb14e9..f24ac6902fb 100644 --- a/apps/webapp/app/presenters/v3/AlertChannelListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/AlertChannelListPresenter.server.ts @@ -10,12 +10,9 @@ import { } from "~/models/projectAlert.server"; import { getLimit } from "~/services/platform.v3.server"; -export type AlertChannelListPresenterData = Awaited>; +type AlertChannelListPresenterData = Awaited>; export type AlertChannelListPresenterRecord = AlertChannelListPresenterData["alertChannels"][number]; -export type AlertChannelListPresenterAlertProperties = NonNullable< - AlertChannelListPresenterRecord["properties"] ->; export class AlertChannelListPresenter extends BasePresenter { public async call(projectId: string, environmentType?: RuntimeEnvironmentType) { diff --git a/apps/webapp/app/presenters/v3/ApiAlertChannelPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiAlertChannelPresenter.server.ts index dff916a6aa1..625b37fe493 100644 --- a/apps/webapp/app/presenters/v3/ApiAlertChannelPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiAlertChannelPresenter.server.ts @@ -23,21 +23,21 @@ export const ApiAlertType = z.enum([ export type ApiAlertType = z.infer; -export const ApiAlertEnvironmentType = z.enum(["STAGING", "PRODUCTION"]); +const ApiAlertEnvironmentType = z.enum(["STAGING", "PRODUCTION"]); -export type ApiAlertEnvironmentType = z.infer; +type ApiAlertEnvironmentType = z.infer; export const ApiAlertChannel = z.enum(["email", "webhook"]); export type ApiAlertChannel = z.infer; -export const ApiAlertChannelData = z.object({ +const ApiAlertChannelData = z.object({ email: z.string().optional(), url: z.string().optional(), secret: z.string().optional(), }); -export type ApiAlertChannelData = z.infer; +type ApiAlertChannelData = z.infer; export const ApiCreateAlertChannel = z.object({ alertTypes: ApiAlertType.array(), diff --git a/apps/webapp/app/presenters/v3/ApiErrorGroupPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiErrorGroupPresenter.server.ts index 2ecf7cccc86..cbcad90d7d5 100644 --- a/apps/webapp/app/presenters/v3/ApiErrorGroupPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiErrorGroupPresenter.server.ts @@ -38,7 +38,7 @@ function parseClickHouseDateTime(value: string): Date { return new Date(value.replace(" ", "T") + "Z"); } -export class ApiErrorGroupPresenter extends BasePresenter { +class ApiErrorGroupPresenter extends BasePresenter { /** * Resolves a single error group to its API detail shape, or `undefined` if no * such fingerprint exists in the environment (the route turns that into 404). diff --git a/apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts index d1c247939d3..7ebde680b62 100644 --- a/apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts @@ -109,7 +109,7 @@ export class ApiWebhookDeliveryListPresenter extends BasePresenter { } } -export class ApiWebhookDeliveryPresenter extends BasePresenter { +class ApiWebhookDeliveryPresenter extends BasePresenter { public async call( environment: { id: string; projectId: string; organizationId: string }, deliveryFriendlyId: string diff --git a/apps/webapp/app/presenters/v3/ApiWebhookEndpointPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiWebhookEndpointPresenter.server.ts index 07881b018e2..d1378288625 100644 --- a/apps/webapp/app/presenters/v3/ApiWebhookEndpointPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiWebhookEndpointPresenter.server.ts @@ -80,7 +80,7 @@ export class ApiWebhookEndpointListPresenter extends BasePresenter { } } -export class ApiWebhookEndpointPresenter extends BasePresenter { +class ApiWebhookEndpointPresenter extends BasePresenter { public async call( environmentId: string, endpointFriendlyId: string diff --git a/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts b/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts index 4596b08f02c..5440b602877 100644 --- a/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts @@ -26,8 +26,6 @@ export type BatchListOptions = { const DEFAULT_PAGE_SIZE = 25; export type BatchList = Awaited>; -export type BatchListItem = BatchList["batches"][0]; -export type BatchListAppliedFilters = BatchList["filters"]; // The row shape of the raw BatchTaskRun keyset scan. Extracted to a named type so the // store-selected scan closure and the keyset merge in `#scanBatchTaskRun` can reference it. diff --git a/apps/webapp/app/presenters/v3/BatchPresenter.server.ts b/apps/webapp/app/presenters/v3/BatchPresenter.server.ts index 3e9ef0be858..dc4187c0523 100644 --- a/apps/webapp/app/presenters/v3/BatchPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/BatchPresenter.server.ts @@ -31,8 +31,6 @@ type BatchPresenterDeps = { resolveDisplayableEnvironment?: typeof findDisplayableEnvironment; }; -export type BatchPresenterData = Awaited>; - export class BatchPresenter extends BasePresenter { constructor( _prisma?: PrismaClientOrTransaction, diff --git a/apps/webapp/app/presenters/v3/BranchesPresenter.server.ts b/apps/webapp/app/presenters/v3/BranchesPresenter.server.ts index 8cdd50ada8c..06dad762b0c 100644 --- a/apps/webapp/app/presenters/v3/BranchesPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/BranchesPresenter.server.ts @@ -16,9 +16,6 @@ import { toBranchableEnvironmentType, } from "~/utils/branchableEnvironment"; -type Result = Awaited>; -export type Branch = Result["branches"][number]; - const BRANCHES_PER_PAGE = 25; /** diff --git a/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts b/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts index 6d758f6b5ad..f73366a5dac 100644 --- a/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts @@ -17,7 +17,7 @@ import { const pageSize = 20; -export type DeploymentList = Awaited>; +type DeploymentList = Awaited>; export type DeploymentListItem = DeploymentList["deployments"][0]; export class DeploymentListPresenter { diff --git a/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts b/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts index d1eb4740045..4a217b568bc 100644 --- a/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts @@ -12,7 +12,7 @@ import { boundedIn, type Prisma } from "@trigger.dev/database"; type Result = Awaited>; export type EnvironmentVariableWithSetValues = Result["environmentVariables"][number]; -export const DEFAULT_ENV_VARS_PAGE_SIZE = 50; +const DEFAULT_ENV_VARS_PAGE_SIZE = 50; export class EnvironmentVariablesPresenter { #prismaClient: PrismaClient; diff --git a/apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts b/apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts index d2f6bbfcbe3..6eb299a5e7b 100644 --- a/apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts @@ -1,10 +1,9 @@ -import { z } from "zod"; import { type ClickHouse, msToClickHouseInterval } from "@internal/clickhouse"; import { TimeGranularity } from "~/utils/timeGranularity"; import { ErrorId } from "@trigger.dev/core/v3/isomorphic"; import { type ErrorGroupStatus, type PrismaClientOrTransaction } from "@trigger.dev/database"; import { timeFilterFromTo } from "~/components/runs/v3/SharedFilters"; -import { type Direction, DirectionSchema } from "~/components/ListPagination"; +import { type Direction } from "~/components/ListPagination"; import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { BasePresenter } from "~/presenters/v3/basePresenter.server"; @@ -36,23 +35,8 @@ export type ErrorGroupOptions = { direction?: Direction; }; -export const ErrorGroupOptionsSchema = z.object({ - userId: z.string().optional(), - projectId: z.string(), - fingerprint: z.string(), - versions: z.array(z.string()).optional(), - runsPageSize: z.number().int().positive().max(1000).optional(), - period: z.string().optional(), - from: z.number().int().nonnegative().optional(), - to: z.number().int().nonnegative().optional(), - cursor: z.string().optional(), - direction: DirectionSchema.optional(), -}); - const DEFAULT_RUNS_PAGE_SIZE = 25; -export type ErrorGroupDetail = Awaited>; - function parseClickHouseDateTime(value: string): Date { const asNum = Number(value); if (!isNaN(asNum) && asNum > 1e12) { diff --git a/apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts b/apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts index 76a2319fee4..55c675cb743 100644 --- a/apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts @@ -41,28 +41,10 @@ export type ErrorsListOptions = { pageSize?: number; }; -export const ErrorsListOptionsSchema = z.object({ - userId: z.string().optional(), - projectId: z.string(), - tasks: z.array(z.string()).optional(), - versions: z.array(z.string()).optional(), - statuses: z.array(z.enum(["UNRESOLVED", "RESOLVED", "IGNORED"])).optional(), - period: z.string().optional(), - from: z.number().int().nonnegative().optional(), - to: z.number().int().nonnegative().optional(), - defaultPeriod: z.string().optional(), - retentionLimitDays: z.number().int().positive().optional(), - search: z.string().max(1000).optional(), - direction: z.enum(["forward", "backward"]).optional(), - cursor: z.string().optional(), - pageSize: z.number().int().positive().max(1000).optional(), -}); - const DEFAULT_PAGE_SIZE = 25; export type ErrorsList = Awaited>; export type ErrorGroup = ErrorsList["errorGroups"][0]; -export type ErrorsListAppliedFilters = ErrorsList["filters"]; export type ErrorOccurrences = Awaited>; export type ErrorOccurrenceActivity = ErrorOccurrences["data"][string]; diff --git a/apps/webapp/app/presenters/v3/LogDetailPresenter.server.ts b/apps/webapp/app/presenters/v3/LogDetailPresenter.server.ts index 4aff7c49d8b..b2cb716ad32 100644 --- a/apps/webapp/app/presenters/v3/LogDetailPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/LogDetailPresenter.server.ts @@ -15,8 +15,6 @@ export type LogDetailOptions = { startTime: string; }; -export type LogDetail = Awaited>; - export class LogDetailPresenter { constructor( private readonly replica: PrismaClientOrTransaction, diff --git a/apps/webapp/app/presenters/v3/LogsListPresenter.server.ts b/apps/webapp/app/presenters/v3/LogsListPresenter.server.ts index 9c19cb75715..50185d12b98 100644 --- a/apps/webapp/app/presenters/v3/LogsListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/LogsListPresenter.server.ts @@ -72,9 +72,8 @@ export const LogsListOptionsSchema = z.object({ const DAY_MS = 24 * 60 * 60 * 1000; -export type LogsList = Awaited>; +type LogsList = Awaited>; export type LogEntry = LogsList["logs"][0]; -export type LogsListAppliedFilters = LogsList["filters"]; // Bump when the cursor shape changes so stale cursors are ignored (reset to the first page) // rather than misparsed. diff --git a/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts b/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts index 0b84e971b2f..445313dcf70 100644 --- a/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts @@ -1,26 +1,10 @@ import { BasePresenter } from "./basePresenter.server"; -import { type QueryScope } from "~/services/queryService.server"; import { getLimit } from "~/services/platform.v3.server"; import { z } from "zod"; import { fromZodError } from "zod-validation-error"; import { builtInDashboard } from "./BuiltInDashboards.server"; import { QueryWidgetConfig } from "~/components/metrics/QueryWidget"; -export type MetricFilters = { - /** Org, project, environment */ - scope: QueryScope; - /** Time filter settings */ - filterPeriod: string | null; - filterFrom: Date | null; - filterTo: Date | null; - /** Tasks */ - taskIdentifiers?: string[]; - /** Queues */ - queues?: string[]; - /** Tags */ - tags?: string[]; -}; - export const LayoutItem = z.object({ i: z.string(), x: z.number(), diff --git a/apps/webapp/app/presenters/v3/ModelRegistryPresenter.server.ts b/apps/webapp/app/presenters/v3/ModelRegistryPresenter.server.ts index 90cab7cb914..cd092f60bf7 100644 --- a/apps/webapp/app/presenters/v3/ModelRegistryPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ModelRegistryPresenter.server.ts @@ -61,7 +61,7 @@ function inferProvider(modelName: string): string { } /** Format a model as provider:name (e.g. "openai:gpt-5"). */ -export function formatModelId(provider: string, modelName: string): string { +function formatModelId(provider: string, modelName: string): string { return `${provider}:${modelName}`; } @@ -138,7 +138,7 @@ export type ModelCatalogItem = { variants: ModelVariant[]; }; -export type ModelVariant = { +type ModelVariant = { friendlyId: string; modelName: string; displayId: string; diff --git a/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts b/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts index 17ccc68a03b..0dc3daa9856 100644 --- a/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts @@ -17,7 +17,7 @@ const MAX_ITEMS_PER_PAGE = 100; export type QueueListSort = "busiest" | "queued" | "name"; /** Ranking reads recent aggregated gauges, so ordering is a stable snapshot, not a live sort. */ -export const QUEUE_RANKING_WINDOW_MINUTES = 15; +const QUEUE_RANKING_WINDOW_MINUTES = 15; const MAX_RANKED_QUEUES = 5000; const typeToDBQueueType: Record<"task" | "custom", TaskQueueType> = { diff --git a/apps/webapp/app/presenters/v3/RunPresenter.server.ts b/apps/webapp/app/presenters/v3/RunPresenter.server.ts index 8a99f3e60f9..36e264235a2 100644 --- a/apps/webapp/app/presenters/v3/RunPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunPresenter.server.ts @@ -13,10 +13,6 @@ import { runStore } from "~/v3/runStore.server"; import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server"; import { runTriggeredAt } from "~/v3/runTimestamps"; -type Result = Awaited>; -export type Run = Result["run"]; -export type RunEvent = NonNullable["events"][0]; - export class RunEnvironmentMismatchError extends Error { constructor(message: string) { super(message); diff --git a/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts b/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts index 59f4e1047ef..f3761ee7fd3 100644 --- a/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts @@ -19,9 +19,6 @@ export type TagListOptions = { const DEFAULT_PAGE_SIZE = 25; -export type TagList = Awaited>; -export type TagListItem = TagList["tags"][number]; - export class RunTagListPresenter extends BasePresenter { public async call({ organizationId, diff --git a/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts b/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts index c0eb6fc7a37..e81918ddb03 100644 --- a/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts @@ -20,7 +20,7 @@ type ScheduleListOptions = { const DEFAULT_PAGE_SIZE = 20; -export type ScheduleListItem = { +type ScheduleListItem = { id: string; type: ScheduleType; friendlyId: string; @@ -43,8 +43,6 @@ export type ScheduleListItem = { branchName?: string; }[]; }; -export type ScheduleList = Awaited>; -export type ScheduleListAppliedFilters = ScheduleList["filters"]; export class ScheduleListPresenter extends BasePresenter { public async call({ diff --git a/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts b/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts index 1e6d1fa2391..0c4ec78e8ef 100644 --- a/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/SessionListPresenter.server.ts @@ -40,7 +40,6 @@ const DEFAULT_PAGE_SIZE = 25; export type SessionList = Awaited>; export type SessionListItem = SessionList["sessions"][0]; -export type SessionListAppliedFilters = SessionList["filters"]; export class SessionListPresenter { constructor( diff --git a/apps/webapp/app/presenters/v3/SessionPresenter.server.ts b/apps/webapp/app/presenters/v3/SessionPresenter.server.ts index 5f0c0466cb9..b367c521f84 100644 --- a/apps/webapp/app/presenters/v3/SessionPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/SessionPresenter.server.ts @@ -11,8 +11,6 @@ import { runStore } from "~/v3/runStore.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { startActiveSpan } from "~/v3/tracer.server"; -export type SessionDetail = NonNullable>>; - export class SessionPresenter { constructor(private readonly replica: PrismaClientOrTransaction) {} diff --git a/apps/webapp/app/presenters/v3/TaskDetailPresenter.server.ts b/apps/webapp/app/presenters/v3/TaskDetailPresenter.server.ts index d4bd38cf643..df73464cfd4 100644 --- a/apps/webapp/app/presenters/v3/TaskDetailPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TaskDetailPresenter.server.ts @@ -15,14 +15,14 @@ import { zeroFillGroupedSeries, } from "./activitySeries.server"; -export type TaskDetailQueue = { +type TaskDetailQueue = { friendlyId: string; name: string; concurrencyLimit: number | null; paused: boolean; }; -export type TaskDetailRetry = { +type TaskDetailRetry = { maxAttempts?: number; factor?: number; minTimeoutInMs?: number; @@ -47,7 +47,7 @@ export type TaskDetail = { hasPayloadSchema: boolean; }; -export type TaskActivityPoint = { +type TaskActivityPoint = { bucket: number; } & Record; diff --git a/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts b/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts index 1541329884f..4b268a56c5e 100644 --- a/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts @@ -19,7 +19,7 @@ export type TaskListItem = { triggerSource: TaskTriggerSource; }; -export class TaskListPresenter { +class TaskListPresenter { constructor(private readonly _replica: PrismaClientOrTransaction) {} public async call({ diff --git a/apps/webapp/app/presenters/v3/TaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TaskPresenter.server.ts deleted file mode 100644 index e000c8dc41c..00000000000 --- a/apps/webapp/app/presenters/v3/TaskPresenter.server.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { BackgroundWorkerTask } from "@trigger.dev/database"; -import type { PrismaClient } from "~/db.server"; -import { prisma } from "~/db.server"; -import type { Project } from "~/models/project.server"; -import type { User } from "~/models/user.server"; - -export class TaskPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - taskFriendlyId, - projectSlug, - }: { - userId: User["id"]; - taskFriendlyId: BackgroundWorkerTask["friendlyId"]; - projectSlug: Project["slug"]; - }) { - const task = await this.#prismaClient.backgroundWorkerTask.findFirst({ - select: { - id: true, - slug: true, - filePath: true, - friendlyId: true, - createdAt: true, - worker: { - select: { - id: true, - version: true, - sdkVersion: true, - cliVersion: true, - createdAt: true, - updatedAt: true, - friendlyId: true, - }, - }, - runtimeEnvironment: { - select: { - id: true, - slug: true, - type: true, - orgMember: { - select: { - user: { - select: { - id: true, - name: true, - displayName: true, - }, - }, - }, - }, - }, - }, - }, - where: { - friendlyId: taskFriendlyId, - runtimeEnvironment: { - organization: { - members: { - some: { - userId, - }, - }, - }, - }, - project: { - slug: projectSlug, - }, - }, - }); - - if (!task) { - return undefined; - } - - return task; - } -} diff --git a/apps/webapp/app/presenters/v3/TasksDashboardPresenter.server.ts b/apps/webapp/app/presenters/v3/TasksDashboardPresenter.server.ts index 83b9c6afc84..4cdfa052862 100644 --- a/apps/webapp/app/presenters/v3/TasksDashboardPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TasksDashboardPresenter.server.ts @@ -8,15 +8,13 @@ import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.s const DAYS = 7; -export type TaskKind = "AGENT" | "STANDARD" | "SCHEDULED"; - export type DailyRunPoint = { /** ISO date (YYYY-MM-DD, UTC) */ day: string; count: number; }; -export type TasksDashboardResult = { +type TasksDashboardResult = { counts: { agents: number; standard: number; @@ -29,7 +27,7 @@ export type TasksDashboardResult = { }>; }; -export class TasksDashboardPresenter { +class TasksDashboardPresenter { constructor(private readonly _replica: PrismaClientOrTransaction) {} public async call({ diff --git a/apps/webapp/app/presenters/v3/TestPresenter.server.ts b/apps/webapp/app/presenters/v3/TestPresenter.server.ts index 22cac2c384f..7c5495064ea 100644 --- a/apps/webapp/app/presenters/v3/TestPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestPresenter.server.ts @@ -10,7 +10,7 @@ type TaskListOptions = { environmentType: RuntimeEnvironmentType; }; -export type TaskList = Awaited>; +type TaskList = Awaited>; export type TaskListItem = NonNullable[0]; export class TestPresenter extends BasePresenter { diff --git a/apps/webapp/app/presenters/v3/UnifiedTaskListPresenter.server.ts b/apps/webapp/app/presenters/v3/UnifiedTaskListPresenter.server.ts index f0508d9eed0..243bf7a9fec 100644 --- a/apps/webapp/app/presenters/v3/UnifiedTaskListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/UnifiedTaskListPresenter.server.ts @@ -33,7 +33,7 @@ export type UnifiedRunningStates = Record; /** One hour bucket: the bucket start date, a total count for axis scaling, * and per-status counts (sparse — only statuses that occurred are present). */ -export type HourlyTaskActivityBucket = { +type HourlyTaskActivityBucket = { date: Date; total: number; } & Partial>; @@ -41,7 +41,7 @@ export type HourlyTaskActivityBucket = { /** 24h hourly stacked-by-status series keyed by task slug. */ export type HourlyTaskActivity = Record; -export class UnifiedTaskListPresenter { +class UnifiedTaskListPresenter { constructor(private readonly _replica: PrismaClientOrTransaction) {} public async call(args: { diff --git a/apps/webapp/app/presenters/v3/UsagePresenter.server.ts b/apps/webapp/app/presenters/v3/UsagePresenter.server.ts index a3b96cf6ac3..cec59f1048f 100644 --- a/apps/webapp/app/presenters/v3/UsagePresenter.server.ts +++ b/apps/webapp/app/presenters/v3/UsagePresenter.server.ts @@ -15,16 +15,6 @@ type Options = { startDate: Date; }; -export type TaskUsageItem = { - taskIdentifier: string; - runCount: number; - averageDuration: number; - averageCost: number; - totalDuration: number; - totalCost: number; - totalBaseCost: number; -}; - export type UsageSeriesData = { date: string; dollars: number; diff --git a/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts b/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts index 4b684949952..10a46c01b3a 100644 --- a/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts @@ -50,7 +50,7 @@ export type VercelSettingsResult = { currentTriggerVersionFetchFailed?: boolean; }; -export type VercelAvailableProject = { +type VercelAvailableProject = { id: string; name: string; }; diff --git a/apps/webapp/app/presenters/v3/WaitpointTagListPresenter.server.ts b/apps/webapp/app/presenters/v3/WaitpointTagListPresenter.server.ts index d17105076f4..0992c119a59 100644 --- a/apps/webapp/app/presenters/v3/WaitpointTagListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/WaitpointTagListPresenter.server.ts @@ -12,9 +12,6 @@ export type TagListOptions = { const DEFAULT_PAGE_SIZE = 25; -export type TagList = Awaited>; -export type TagListItem = TagList["tags"][number]; - export class WaitpointTagListPresenter extends BasePresenter { constructor( prismaClient?: PrismaClientOrTransaction, diff --git a/apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts b/apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts index b91b2f104ca..4324e1f670a 100644 --- a/apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts @@ -18,7 +18,7 @@ import { type WebhookComposerEndpointData, } from "./webhookComposerEndpoints.server"; -export type WebhookEndpointSummary = { +type WebhookEndpointSummary = { id: string; opaqueId: string; status: string; @@ -35,7 +35,7 @@ export type WebhookDetail = { endpoint: WebhookEndpointSummary; }; -export type WebhookActivityPoint = { +type WebhookActivityPoint = { bucket: number; // epoch ms } & Record; diff --git a/apps/webapp/app/presenters/v3/dashboardAgent/watch-wording.ts b/apps/webapp/app/presenters/v3/dashboardAgent/watch-wording.ts index a0da0585206..3733550d2b0 100644 --- a/apps/webapp/app/presenters/v3/dashboardAgent/watch-wording.ts +++ b/apps/webapp/app/presenters/v3/dashboardAgent/watch-wording.ts @@ -7,33 +7,20 @@ */ export { formatWatchCadence, - formatWatchDuration, - formatWatchSla, - formatWatchWait, formatWatchWindow, immediateWatchMessage, noteFor, presentResolvedWatch, WATCH_IN_CHAT_DELIVERY_LINE, WATCH_PRESENTATION_FALLBACK, - WATCH_UPDATE_LABEL, shortFingerprint, watchConditionLabel, - watchConditionWording, watchConfirmationBlockBody, watchDurationLabel, - watchExternalNotificationLine, - watchFollowUpLines, watchIdentityValue, - watchLifetimeSentence, watchNoteLine, watchOneShotBlockBody, - watchRequestSentence, watchSubjectLabel, - watchSubline, watchTooltipLabel, - type WatchConditionWording, - type WatchPresentation, type WatchResolvedInput, - type WatchSemanticIcon, } from "@internal/dashboard-agent-contracts"; diff --git a/apps/webapp/app/presenters/v3/queueListPagination.server.ts b/apps/webapp/app/presenters/v3/queueListPagination.server.ts index b366ebe0f4e..44e9c3dafed 100644 --- a/apps/webapp/app/presenters/v3/queueListPagination.server.ts +++ b/apps/webapp/app/presenters/v3/queueListPagination.server.ts @@ -1,10 +1,10 @@ -export type QueueListFilteredPagination = { +type QueueListFilteredPagination = { mode: "filtered"; currentPage: number; hasMore: boolean; }; -export type QueueListUnfilteredPagination = { +type QueueListUnfilteredPagination = { mode: "unfiltered"; currentPage: number; totalPages: number; diff --git a/apps/webapp/app/presenters/v3/reports/health/execution.ts b/apps/webapp/app/presenters/v3/reports/health/execution.ts index 4357c680dcc..0df0b3e336c 100644 --- a/apps/webapp/app/presenters/v3/reports/health/execution.ts +++ b/apps/webapp/app/presenters/v3/reports/health/execution.ts @@ -1,7 +1,7 @@ import { isOk, maxSeverity, type Finding, type Metric } from "../report-view-model"; import { HEALTH_THRESHOLDS, metricById, type HealthInput } from "./health-core"; -export const EXECUTION_METRIC_IDS = ["failures", "dur_p95"]; +const EXECUTION_METRIC_IDS = ["failures", "dur_p95"]; export function interpretExecution(metrics: Metric[], input: HealthInput): Finding { const exec = EXECUTION_METRIC_IDS.map((id) => metricById(metrics, id)); diff --git a/apps/webapp/app/presenters/v3/reports/health/flow.ts b/apps/webapp/app/presenters/v3/reports/health/flow.ts index e8cf27f915c..ce6ba724527 100644 --- a/apps/webapp/app/presenters/v3/reports/health/flow.ts +++ b/apps/webapp/app/presenters/v3/reports/health/flow.ts @@ -19,7 +19,7 @@ import { type HealthInput, } from "./health-core"; -export const FLOW_METRIC_IDS = ["start_latency_p95", "pending", "throughput"]; +const FLOW_METRIC_IDS = ["start_latency_p95", "pending", "throughput"]; /** Unmeasurable backlog: verdict is unassessable. Distinct from "unknown", the staleness guard. */ export const FLOW_UNMEASURED = "flow_unmeasured"; diff --git a/apps/webapp/app/presenters/v3/reports/health/health-data.ts b/apps/webapp/app/presenters/v3/reports/health/health-data.ts index ea2d54c51aa..3c14b498e57 100644 --- a/apps/webapp/app/presenters/v3/reports/health/health-data.ts +++ b/apps/webapp/app/presenters/v3/reports/health/health-data.ts @@ -263,7 +263,7 @@ async function tryQuery( } } -export type FlowData = { +type FlowData = { flowSource: HealthInput["flowSource"]; pending: HealthInput["pending"]; startLatency: HealthInput["startLatency"]; @@ -286,12 +286,12 @@ type RunsContext = { liveScalar: Row; liveSeries: Row[]; baselineScalar: Row }; * "unavailable" is a recognized rollout state, so the next source down is a legitimate substitute. * "failed" is anything else and must make the flow verdict unassessable, never fall through to it. */ -export type FlowLoadResult = +type FlowLoadResult = | { status: "ok"; data: FlowData } | { status: "unavailable" } | { status: "failed"; error: unknown }; -export interface FlowSource { +interface FlowSource { loadFlow( env: AuthenticatedEnvironment, period: string, @@ -334,7 +334,7 @@ function isRolloutError(error: unknown): boolean { } /** Measured depth and scheduling-delay p95 from `env_metrics`. Unavailable until it is populated. */ -export const QueueMetricsSource: FlowSource = { +const QueueMetricsSource: FlowSource = { async loadFlow(env, period, ctx, deps) { try { // The rejection must be guarded: if the queries below throw first this is never awaited, and @@ -495,7 +495,7 @@ function buildQueueMetricsFlow(args: { * Fallback: live Redis depth plus a backlog proxy from `runs` (triggered minus finished). The proxy * is shape-only: it starts at 0 within the window and can't see backlog that predates it. */ -export const SnapshotFlowSource: FlowSource = { +const SnapshotFlowSource: FlowSource = { async loadFlow(env, _period, ctx, deps) { // Last-resort source, so a Redis failure must not break the report. const pendingNow = await deps.lengthOfEnvQueue(env).catch(() => undefined); diff --git a/apps/webapp/app/presenters/v3/reports/health/health.ts b/apps/webapp/app/presenters/v3/reports/health/health.ts index 1ee163fed77..4780064b443 100644 --- a/apps/webapp/app/presenters/v3/reports/health/health.ts +++ b/apps/webapp/app/presenters/v3/reports/health/health.ts @@ -129,7 +129,7 @@ function collectLinks(findings: Finding[]): ReportViewModel["links"] { } // The health verdict. All health semantics live here and no presentation does. -export type HealthAssessment = { +type HealthAssessment = { scope: string; period: string; baselineLabel: string; @@ -147,7 +147,7 @@ export type HealthAssessment = { facts: Record; }; -export function assessHealth(input: HealthInput): HealthAssessment { +function assessHealth(input: HealthInput): HealthAssessment { const metrics = buildMetrics(input); const drain = computeDrain(input); diff --git a/apps/webapp/app/presenters/v3/reports/report-layout.ts b/apps/webapp/app/presenters/v3/reports/report-layout.ts index 35e17a9e932..10e297e6adf 100644 --- a/apps/webapp/app/presenters/v3/reports/report-layout.ts +++ b/apps/webapp/app/presenters/v3/reports/report-layout.ts @@ -96,8 +96,6 @@ export const REPORT_SECTION_ORDER = [ "footer", ] as const; -export type ReportSectionId = (typeof REPORT_SECTION_ORDER)[number]; - /** * Reasons that mean "we can't say" rather than a verdict, so their finding renders headline-only. * A measured finding never carries one: an unmeasured input costs its own metric, not the verdict. @@ -125,11 +123,11 @@ export function reportTrust(vm: { facts?: Record }): LayoutTrus return (typeof reason === "string" ? TRUST_CAVEATS[reason] : undefined) ?? TRUST_CAVEAT_FALLBACK; } -export function reportTone(severity: Severity, reason?: string): ReportTone { +function reportTone(severity: Severity, reason?: string): ReportTone { return reason !== undefined && NEUTRAL_REASONS.has(reason) ? "neutral" : severity; } -export function reportGlyph(severity: Severity, reason?: string): string { +function reportGlyph(severity: Severity, reason?: string): string { return REPORT_GLYPH[reportTone(severity, reason)]; } @@ -160,11 +158,11 @@ export function reportFooterStyle(code: string): ReportFooterStyle { const MINUS = "−"; // U+2212 -export function fmtCount(n: number): string { +function fmtCount(n: number): string { return Math.round(n).toLocaleString("en-US"); } -export function fmtDuration(ms: number): string { +function fmtDuration(ms: number): string { if (ms < 1000) return `${Math.round(ms)}ms`; const s = ms / 1000; if (s < 60) return Number.isInteger(s) ? `${s}s` : `${s.toFixed(1)}s`; @@ -172,16 +170,16 @@ export function fmtDuration(ms: number): string { return Number.isInteger(m) ? `${m}m` : `${m.toFixed(1)}m`; } -export function fmtPct(ratio: number): string { +function fmtPct(ratio: number): string { return `${(ratio * 100).toFixed(1)}%`; } -export function fmtRate(n: number): string { +function fmtRate(n: number): string { return `${fmtCount(n)}/min`; } /** A net rate carries its sign; a plain rate does not, so it isn't read as a change. */ -export function fmtSignedRate(net: number): string { +function fmtSignedRate(net: number): string { const sign = net < 0 ? MINUS : net > 0 ? "+" : ""; return `${sign}${fmtCount(Math.abs(net))}/min`; } @@ -200,10 +198,7 @@ export function fmtValue(value: number, unit: Unit): string { } /** Fill the `{token}` placeholders a message catalog leaves for the renderer. */ -export function fillTokens( - template: string, - tokens: Record -): string { +function fillTokens(template: string, tokens: Record): string { return template.replace(/\{(\w+)\}/g, (whole, key: string) => { const value = tokens[key]; if (value === undefined) return whole; @@ -230,7 +225,7 @@ export type LayoutMetricInput = { severity: Severity; }; -export type LayoutFindingInput = { +type LayoutFindingInput = { type: string; severity: Severity; reason: string; @@ -260,10 +255,10 @@ export type LayoutViewModel = { // --- output shapes ---------------------------------------------------------- -export type LayoutDelta = { text: string; dir: "up" | "down" | "flat" }; +type LayoutDelta = { text: string; dir: "up" | "down" | "flat" }; /** A metric's aside. `kind` lets a renderer choose its own frame around shared wording. */ -export type LayoutNote = { kind: "annotation" | "baseline" | "estimated"; text: string }; +type LayoutNote = { kind: "annotation" | "baseline" | "estimated"; text: string }; export type LayoutMetricRow = { id: string; @@ -301,9 +296,9 @@ export type LayoutFinding = { attributionKey?: string; }; -export type LayoutStatement = { tone: ReportTone; glyph: string; severity: Severity; text: string }; +type LayoutStatement = { tone: ReportTone; glyph: string; severity: Severity; text: string }; -export type LayoutFooterEntry = { +type LayoutFooterEntry = { code: string; style: ReportFooterStyle; label: string; diff --git a/apps/webapp/app/presenters/v3/reports/report-view-model.ts b/apps/webapp/app/presenters/v3/reports/report-view-model.ts index bbd07effa93..1031141480c 100644 --- a/apps/webapp/app/presenters/v3/reports/report-view-model.ts +++ b/apps/webapp/app/presenters/v3/reports/report-view-model.ts @@ -6,10 +6,7 @@ import { type ReportExclusion, type ReportFinding, type ReportFooterEntry, - type ReportLink as CoreReportLink, - type ReportLinkKey, type ReportMetric, - type ReportMetricSeries, type ReportObservation, type ReportReasonCode, type ReportRecommendation, @@ -23,10 +20,7 @@ export type Severity = ReportSeverity; export type Unit = ReportUnit; /** A code resolved to a human string by `report-messages.ts`. */ export type ReasonCode = ReportReasonCode; -/** A key into `ReportViewModel.links`, so a recommendation can point at a URL. */ -export type LinkKey = ReportLinkKey; export type Delta = ReportDelta; -export type MetricSeries = ReportMetricSeries; export type Metric = ReportMetric; export type Recommendation = ReportRecommendation; export type FooterEntry = ReportFooterEntry; @@ -34,7 +28,6 @@ export type Exclusion = ReportExclusion; export type Observation = ReportObservation; export type Finding = ReportFinding; export type SummaryStatement = ReportSummaryStatement; -export type ReportLink = CoreReportLink; export type ReportViewModel = CoreReportViewModel; /** Direction and rounded multiplier of `value` against a `normal` baseline. */ diff --git a/apps/webapp/app/runEngine/concerns/computeMigration.server.ts b/apps/webapp/app/runEngine/concerns/computeMigration.server.ts index e598cbdca72..f29bc2e8370 100644 --- a/apps/webapp/app/runEngine/concerns/computeMigration.server.ts +++ b/apps/webapp/app/runEngine/concerns/computeMigration.server.ts @@ -1,7 +1,7 @@ import { hashBucket } from "~/utils/computeBucket"; /** Subset of the global flags snapshot this resolver reads. */ -export type ComputeMigrationFlags = { +type ComputeMigrationFlags = { computeMigrationEnabled?: boolean; computeMigrationFreePercentage?: number; computeMigrationPaidPercentage?: number; diff --git a/apps/webapp/app/runEngine/concerns/queues.server.ts b/apps/webapp/app/runEngine/concerns/queues.server.ts index 85052ad60ae..1374a34d288 100644 --- a/apps/webapp/app/runEngine/concerns/queues.server.ts +++ b/apps/webapp/app/runEngine/concerns/queues.server.ts @@ -482,9 +482,7 @@ export class DefaultQueueManager implements QueueManager { } } -export function getMaximumSizeForEnvironment( - environment: AuthenticatedEnvironment -): number | undefined { +function getMaximumSizeForEnvironment(environment: AuthenticatedEnvironment): number | undefined { if (environment.type === "DEVELOPMENT") { return environment.organization.maximumDevQueueSize ?? env.MAXIMUM_DEV_QUEUE_SIZE; } else { diff --git a/apps/webapp/app/runEngine/services/streamBatchItems.server.ts b/apps/webapp/app/runEngine/services/streamBatchItems.server.ts index 05498066f98..9437aab9633 100644 --- a/apps/webapp/app/runEngine/services/streamBatchItems.server.ts +++ b/apps/webapp/app/runEngine/services/streamBatchItems.server.ts @@ -41,7 +41,7 @@ import { BatchPayloadProcessor } from "../concerns/batchPayloads.server"; * at the run level, so the trigger call must throw to give their retry/ * error handling a chance to create a fresh batch. */ -export function isIdempotentRetrySuccess( +function isIdempotentRetrySuccess( status: BatchTaskRunStatus | null | undefined, sealed: boolean | null | undefined, processingCompletedAt: Date | null | undefined diff --git a/apps/webapp/app/runEngine/types.ts b/apps/webapp/app/runEngine/types.ts index 14c992a2852..4e415483120 100644 --- a/apps/webapp/app/runEngine/types.ts +++ b/apps/webapp/app/runEngine/types.ts @@ -3,7 +3,7 @@ import type { IOPacket, TaskRunError, TriggerTaskRequestBody } from "@trigger.de import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import type { ReportUsagePlan } from "@trigger.dev/platform"; -export type TriggerTaskServiceOptions = { +type TriggerTaskServiceOptions = { idempotencyKey?: string; idempotencyKeyExpiresAt?: Date; triggerVersion?: string; @@ -31,12 +31,6 @@ export type TriggerTaskRequest = { options?: TriggerTaskServiceOptions; }; -export type TriggerTaskResult = { - run: TaskRun; - isCached: boolean; - error?: TaskRunError; -}; - export type QueueValidationResult = | { ok: true; diff --git a/apps/webapp/app/services/apiAuth.server.ts b/apps/webapp/app/services/apiAuth.server.ts index d0b6449b820..5f7207bb0b7 100644 --- a/apps/webapp/app/services/apiAuth.server.ts +++ b/apps/webapp/app/services/apiAuth.server.ts @@ -1,5 +1,5 @@ import { json } from "@remix-run/server-runtime"; -import { SignJWT, errors, jwtVerify } from "jose"; +import { SignJWT } from "jose"; import { z } from "zod"; import { $replica } from "~/db.server"; @@ -21,9 +21,6 @@ import type { } from "@trigger.dev/rbac"; import { assertUserActorEnvironment } from "./userActorEnvironment.server"; import { type RuntimeEnvironmentForEnvRepo } from "~/v3/environmentVariables/environmentVariablesRepository.server"; -import { logger } from "./logger.server"; -import { safeEnvironmentLogFields } from "./safeEnvironmentLog"; -import { missingJwtLogContext } from "./safeRequestLogContext"; import { type PersonalAccessTokenAuthenticationResult, authenticateApiRequestWithPersonalAccessToken, @@ -92,7 +89,7 @@ export type ApiAuthenticationResultSuccess = { }; }; -export type ApiAuthenticationResultFailure = { +type ApiAuthenticationResultFailure = { ok: false; error: string; }; @@ -871,104 +868,6 @@ export async function generateJWTTokenForEnvironment( return jwt; } -export async function validateJWTTokenAndRenew( - request: Request, - payloadSchema: T -): Promise<{ payload: z.infer; jwt: string } | undefined> { - try { - const jwt = request.headers.get("x-trigger-jwt"); - - if (!jwt) { - // Log a safe breadcrumb, not the raw headers (which carry the - // caller's Authorization credential). - logger.debug("Missing JWT token in request", missingJwtLogContext(request)); - - return; - } - - const { payload: rawPayload } = await jwtVerify(jwt, JWT_SECRET, { - issuer: "https://id.trigger.dev", - audience: "https://api.trigger.dev", - }); - - const payload = payloadSchema.safeParse(rawPayload); - - if (!payload.success) { - logger.error("Failed to validate JWT", { payload: rawPayload, issues: payload.error.issues }); - - return; - } - - const renewedJwt = await renewJWTToken(payload.data); - - return { - payload: payload.data, - jwt: renewedJwt, - }; - } catch (error) { - if (error instanceof errors.JWTExpired) { - // Now we need to try and renew the token using the API key auth - const authenticatedEnv = await authenticateApiRequest(request); - - if (!authenticatedEnv) { - logger.error("Failed to renew JWT token, missing or invalid Authorization header", { - error: error.message, - }); - - return; - } - - if (!authenticatedEnv.ok) { - logger.error("Failed to renew JWT token, invalid API key", { - error: error.message, - }); - - return; - } - - const payload = payloadSchema.safeParse(error.payload); - - if (!payload.success) { - logger.error("Failed to parse jwt payload after expired", { - payload: error.payload, - issues: payload.error.issues, - }); - - return; - } - - const renewedJwt = await generateJWTTokenForEnvironment(authenticatedEnv.environment, { - ...payload.data, - }); - - // The environment carries secret material; log only non-secret fields. - logger.debug("Renewed JWT token from Authorization header API Key", { - environment: safeEnvironmentLogFields(authenticatedEnv.environment), - payload: payload.data, - }); - - return { - payload: payload.data, - jwt: renewedJwt, - }; - } - - logger.error("Failed to validate JWT token", { error }); - } -} - -async function renewJWTToken(payload: Record) { - const jwt = await new SignJWT(payload) - .setProtectedHeader({ alg: JWT_ALGORITHM }) - .setIssuedAt() - .setIssuer("https://id.trigger.dev") - .setAudience("https://api.trigger.dev") - .setExpirationTime(calculateJWTExpiration()) - .sign(JWT_SECRET); - - return jwt; -} - function calculateJWTExpiration() { if (env.PROD_USAGE_HEARTBEAT_INTERVAL_MS) { return ( diff --git a/apps/webapp/app/services/attio.server.ts b/apps/webapp/app/services/attio.server.ts index f0852509f4f..787aa749ace 100644 --- a/apps/webapp/app/services/attio.server.ts +++ b/apps/webapp/app/services/attio.server.ts @@ -112,7 +112,7 @@ function domainFromEmail(email: string | undefined): string | undefined { return email?.split("@")[1]?.toLowerCase().trim() || undefined; } -export const attioClient = env.ATTIO_API_KEY ? new AttioClient(env.ATTIO_API_KEY) : null; +const attioClient = env.ATTIO_API_KEY ? new AttioClient(env.ATTIO_API_KEY) : null; export async function enqueueAttioWorkspaceSync(payload: AttioWorkspaceSync) { if (!attioClient) return; diff --git a/apps/webapp/app/services/authFeatureControls.server.ts b/apps/webapp/app/services/authFeatureControls.server.ts index 5052b6b5a3c..3e562d608e5 100644 --- a/apps/webapp/app/services/authFeatureControls.server.ts +++ b/apps/webapp/app/services/authFeatureControls.server.ts @@ -1,9 +1,6 @@ import { resolveAuthFeatureControls } from "~/services/authFeatureControls"; import { globalFlagsRegistry } from "~/v3/globalFlagsRegistry.server"; -export { resolveAuthFeatureControls } from "~/services/authFeatureControls"; -export type { AuthFeatureControls } from "~/services/authFeatureControls"; - function currentControls() { return resolveAuthFeatureControls(globalFlagsRegistry.current()); } diff --git a/apps/webapp/app/services/authTelemetry.server.ts b/apps/webapp/app/services/authTelemetry.server.ts index 65a545545ea..b19fa6447c2 100644 --- a/apps/webapp/app/services/authTelemetry.server.ts +++ b/apps/webapp/app/services/authTelemetry.server.ts @@ -12,7 +12,7 @@ import { authFeatureControls } from "~/services/authFeatureControls.server"; import { rbac } from "~/services/rbac.server"; import { singleton } from "~/utils/singleton"; -export type ApiAuthResult = "success" | "invalid" | "forbidden" | "disabled" | "error"; +type ApiAuthResult = "success" | "invalid" | "forbidden" | "disabled" | "error"; const telemetry = singleton("apiAuthTelemetry", () => { const meter = getMeter("api-auth"); diff --git a/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts b/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts index ce0b8b50d21..01de2a335cd 100644 --- a/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts +++ b/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts @@ -19,21 +19,21 @@ const DurationSchema = z.custom((value) => { return value as Duration; }); -export const RateLimitFixedWindowConfig = z.object({ +const RateLimitFixedWindowConfig = z.object({ type: z.literal("fixedWindow"), window: DurationSchema, tokens: z.number(), }); -export type RateLimitFixedWindowConfig = z.infer; +type RateLimitFixedWindowConfig = z.infer; -export const RateLimitSlidingWindowConfig = z.object({ +const RateLimitSlidingWindowConfig = z.object({ type: z.literal("slidingWindow"), window: DurationSchema, tokens: z.number(), }); -export type RateLimitSlidingWindowConfig = z.infer; +type RateLimitSlidingWindowConfig = z.infer; export const RateLimitTokenBucketConfig = z.object({ type: z.literal("tokenBucket"), @@ -357,5 +357,3 @@ export function authorizationRateLimitMiddleware({ ); }; } - -export type RateLimitMiddleware = ReturnType; diff --git a/apps/webapp/app/services/autoIncrementCounter.server.ts b/apps/webapp/app/services/autoIncrementCounter.server.ts deleted file mode 100644 index bb9bc339d68..00000000000 --- a/apps/webapp/app/services/autoIncrementCounter.server.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { RedisOptions } from "ioredis"; -import Redis from "ioredis"; -import { defaultReconnectOnError } from "@internal/redis"; -import type { PrismaClientOrTransaction, PrismaTransactionOptions } from "~/db.server"; -import { Prisma, prisma } from "~/db.server"; -import { env } from "~/env.server"; -import { singleton } from "~/utils/singleton"; - -export type AutoIncrementCounterOptions = { - redis: RedisOptions; -}; - -export class AutoIncrementCounter { - private _redis: Redis; - - constructor(private options: AutoIncrementCounterOptions) { - this._redis = new Redis({ reconnectOnError: defaultReconnectOnError, ...options.redis }); - } - - async incrementInTransaction( - key: string, - callback: (num: number, tx: PrismaClientOrTransaction) => Promise, - backfiller?: (key: string, db: PrismaClientOrTransaction) => Promise, - client: PrismaClientOrTransaction = prisma, - transactionOptions?: PrismaTransactionOptions - ): Promise { - let performedIncrement = false; - let performedBackfill = false; - - try { - let newNumber = await this.#increment(key); - - performedIncrement = true; - - if (newNumber === 1 && backfiller) { - const backfilledNumber = await backfiller(key, client); - - if (backfilledNumber && backfilledNumber > 1) { - newNumber = backfilledNumber + 1; - await this._redis.set(key, newNumber); - performedBackfill = true; - } - } - - return await callback(newNumber, client); - } catch (e) { - if ( - e instanceof Prisma.PrismaClientKnownRequestError || - e instanceof Prisma.PrismaClientUnknownRequestError || - e instanceof Prisma.PrismaClientValidationError - ) { - if (performedIncrement && !performedBackfill) { - await this._redis.decr(key); - } - } - - throw e; - } - } - - async #increment(key: string): Promise { - return await this._redis.incr(key); - } -} - -export const autoIncrementCounter = singleton("auto-increment-counter", getAutoIncrementCounter); - -function getAutoIncrementCounter() { - if (!env.REDIS_HOST || !env.REDIS_PORT) { - throw new Error( - "Could not initialize auto-increment counter because process.env.REDIS_HOST and process.env.REDIS_PORT are required to be set. " - ); - } - - return new AutoIncrementCounter({ - redis: { - keyPrefix: "auto-counter:", - port: env.REDIS_PORT, - host: env.REDIS_HOST, - username: env.REDIS_USERNAME, - password: env.REDIS_PASSWORD, - enableAutoPipelining: true, - ...(env.REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }), - }, - }); -} diff --git a/apps/webapp/app/services/betterstack/betterstack.server.ts b/apps/webapp/app/services/betterstack/betterstack.server.ts index 0a097458e40..c7c170de25c 100644 --- a/apps/webapp/app/services/betterstack/betterstack.server.ts +++ b/apps/webapp/app/services/betterstack/betterstack.server.ts @@ -37,7 +37,7 @@ const StatusReportsSchema = z.object({ export type AggregateState = "operational" | "degraded" | "downtime"; -export type IncidentStatus = { +type IncidentStatus = { status: AggregateState; title: string | null; }; diff --git a/apps/webapp/app/services/billingLimit.schemas.ts b/apps/webapp/app/services/billingLimit.schemas.ts index 6628e5cb22c..4571549b5c4 100644 --- a/apps/webapp/app/services/billingLimit.schemas.ts +++ b/apps/webapp/app/services/billingLimit.schemas.ts @@ -9,7 +9,7 @@ import { z } from "zod"; * BillingClient methods. */ -export const BillingLimitStateSchema = z.discriminatedUnion("status", [ +const BillingLimitStateSchema = z.discriminatedUnion("status", [ z.object({ status: z.literal("ok"), }), @@ -27,7 +27,7 @@ export const BillingLimitStateSchema = z.discriminatedUnion("status", [ export type BillingLimitState = z.infer; -export const BillingLimitConfigSchema = z.discriminatedUnion("mode", [ +const BillingLimitConfigSchema = z.discriminatedUnion("mode", [ z.object({ mode: z.literal("none"), }), @@ -42,7 +42,7 @@ export const BillingLimitConfigSchema = z.discriminatedUnion("mode", [ export type BillingLimitConfig = z.infer; -export const BillingLimitUnconfiguredSchema = z.object({ +const BillingLimitUnconfiguredSchema = z.object({ isConfigured: z.literal(false), gracePeriodMs: z.number().int().nonnegative(), }); @@ -55,28 +55,22 @@ const billingLimitConfiguredFields = { gracePeriodMs: z.number().int().nonnegative(), }; -export const BillingLimitConfiguredNoneSchema = z.object({ +const BillingLimitConfiguredNoneSchema = z.object({ ...billingLimitConfiguredFields, mode: z.literal("none"), }); -export const BillingLimitConfiguredPlanSchema = z.object({ +const BillingLimitConfiguredPlanSchema = z.object({ ...billingLimitConfiguredFields, mode: z.literal("plan"), }); -export const BillingLimitConfiguredCustomSchema = z.object({ +const BillingLimitConfiguredCustomSchema = z.object({ ...billingLimitConfiguredFields, mode: z.literal("custom"), amountCents: z.number().int().positive(), }); -export const BillingLimitConfiguredSchema = z.discriminatedUnion("mode", [ - BillingLimitConfiguredNoneSchema, - BillingLimitConfiguredPlanSchema, - BillingLimitConfiguredCustomSchema, -]); - export const BillingLimitResultSchema = z.union([ BillingLimitUnconfiguredSchema, BillingLimitConfiguredNoneSchema, @@ -86,7 +80,7 @@ export const BillingLimitResultSchema = z.union([ export type BillingLimitResult = z.infer; -export const UpdateBillingLimitRequestSchema = z.discriminatedUnion("mode", [ +const UpdateBillingLimitRequestSchema = z.discriminatedUnion("mode", [ z.object({ mode: z.literal("none"), cancelInProgressRuns: z.boolean(), @@ -118,7 +112,7 @@ export const ResolveBillingLimitRequestSchema = z.discriminatedUnion("action", [ export type ResolveBillingLimitRequest = z.infer; -export const BillingLimitActiveOrgSchema = z.object({ +const BillingLimitActiveOrgSchema = z.object({ orgId: z.string(), limitState: z.enum(["grace", "rejected"]), }); @@ -129,7 +123,7 @@ export const BillingLimitsActiveResultSchema = z.object({ export type BillingLimitsActiveResult = z.infer; -export const BillingLimitPendingResolveOrgSchema = z.object({ +const BillingLimitPendingResolveOrgSchema = z.object({ organizationId: z.string(), resumeMode: z.enum(["queue", "new_only"]), resolvedAt: z.string().datetime({ offset: true }), diff --git a/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts b/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts index 2027e8aeca9..ce087279cfa 100644 --- a/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts +++ b/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts @@ -678,14 +678,6 @@ export function getAdminClickhouse(): ClickHouse { return defaultAdminClickhouseClient; } -export function getDefaultClickhouseClient(): ClickHouse { - return defaultClickhouseClient; -} - -export function getDefaultLogsClickhouseClient(): ClickHouse { - return defaultLogsClickhouseClient; -} - /** Queue-metrics client for callers with no organization in scope (the ingestion consumer). */ export function getQueueMetricsClickhouseClient(): ClickHouse { return defaultQueueMetricsClickhouseClient; diff --git a/apps/webapp/app/services/clickhouse/clickhouseSecretSchemas.server.ts b/apps/webapp/app/services/clickhouse/clickhouseSecretSchemas.server.ts index 016eb717c18..fa46992cdd2 100644 --- a/apps/webapp/app/services/clickhouse/clickhouseSecretSchemas.server.ts +++ b/apps/webapp/app/services/clickhouse/clickhouseSecretSchemas.server.ts @@ -3,9 +3,3 @@ import { z } from "zod"; export const ClickhouseConnectionSchema = z.object({ url: z.string().url(), }); - -export type ClickhouseConnection = z.infer; - -export function getClickhouseSecretKey(orgId: string, clientType: string): string { - return `org:${orgId}:clickhouse:${clientType}`; -} diff --git a/apps/webapp/app/services/dashboardAgentAlertUnsubscribeToken.server.ts b/apps/webapp/app/services/dashboardAgentAlertUnsubscribeToken.server.ts index 4c43b962f33..bc7aa298de8 100644 --- a/apps/webapp/app/services/dashboardAgentAlertUnsubscribeToken.server.ts +++ b/apps/webapp/app/services/dashboardAgentAlertUnsubscribeToken.server.ts @@ -15,7 +15,7 @@ const UNSUBSCRIBE_TOKEN_TTL = "365d"; export type UnsubscribeTokenClaims = { channelId: string; alertType: string }; -export async function signDashboardAgentAlertUnsubscribeToken( +async function signDashboardAgentAlertUnsubscribeToken( secret: string, opts: { channelId: string; alertType: string } ): Promise { @@ -33,7 +33,7 @@ export async function signDashboardAgentAlertUnsubscribeToken( return `${UNSUBSCRIBE_TOKEN_PREFIX}${jwt}`; } -export async function verifyDashboardAgentAlertUnsubscribeToken( +async function verifyDashboardAgentAlertUnsubscribeToken( secret: string, token: string ): Promise { diff --git a/apps/webapp/app/services/dashboardAgentBodyCap.server.ts b/apps/webapp/app/services/dashboardAgentBodyCap.server.ts index 185d931584f..0ca6dd65b4d 100644 --- a/apps/webapp/app/services/dashboardAgentBodyCap.server.ts +++ b/apps/webapp/app/services/dashboardAgentBodyCap.server.ts @@ -36,7 +36,7 @@ function refuse(res: Response): void { * reader still receives every chunk while nothing flows until it asks for it. Crossing the * limit ends the request: pausing alone wouldn't stop the route resuming the stream itself. */ -export function capRequestBody(req: Request, res: Response, limit: number): void { +function capRequestBody(req: Request, res: Response, limit: number): void { const declared = Number.parseInt(req.headers["content-length"] ?? "", 10); if (Number.isFinite(declared) && declared > limit) { refuse(res); diff --git a/apps/webapp/app/services/dashboardAgentWatchAlerts.server.ts b/apps/webapp/app/services/dashboardAgentWatchAlerts.server.ts index 0cfe0e7e7bf..9540ab33e7b 100644 --- a/apps/webapp/app/services/dashboardAgentWatchAlerts.server.ts +++ b/apps/webapp/app/services/dashboardAgentWatchAlerts.server.ts @@ -68,7 +68,7 @@ export async function enqueueWatchFiredAlert( }); } -export type DashboardAgentAlertDenyReason = +type DashboardAgentAlertDenyReason = /** The user can't use the dashboard agent, so its watches can't alert either. */ | "dashboard_agent_disabled" /** This installation has no alert email transport configured. */ diff --git a/apps/webapp/app/services/dashboardAgentWatchChecks.ts b/apps/webapp/app/services/dashboardAgentWatchChecks.ts index 717d130ef20..a6ec007a003 100644 --- a/apps/webapp/app/services/dashboardAgentWatchChecks.ts +++ b/apps/webapp/app/services/dashboardAgentWatchChecks.ts @@ -33,23 +33,6 @@ export type { WatchQueueOldestAge, WatchRunRow, } from "./dashboardAgentWatchCheckBase"; -export { - checkRunFailed, - checkRunFinished, - checkRunStart, - describeRunWait, - isTerminalRunStatus, - type WatchWaitBasis, -} from "./dashboardAgentWatchRunChecks"; -export { - checkBacklogDrain, - checkQueueDepthAbove, - checkQueueDepthBelow, - checkQueueOldestAge, - checkQueueStalled, -} from "./dashboardAgentWatchQueueChecks"; -export { checkErrorRecurrence, normalizeErrorFingerprint } from "./dashboardAgentWatchErrorChecks"; -export { checkHealthRecovery } from "./dashboardAgentWatchHealthChecks"; /** * The previous check's facts out of `lastResult`, which holds raw facts, the check endpoint's @@ -114,7 +97,7 @@ export async function checkWatch( * The observation for a check that couldn't run. `verified: false` means the condition * couldn't be confirmed, not that it didn't happen. */ -export function unobservedOutcome(spec: WatchSpec): WatchObservedOutcome { +function unobservedOutcome(spec: WatchSpec): WatchObservedOutcome { switch (spec.kind) { case "run_start": return { kind: "run_start", verified: false, status: null, started: false }; diff --git a/apps/webapp/app/services/dashboardAgentWatchInvestigate.server.ts b/apps/webapp/app/services/dashboardAgentWatchInvestigate.server.ts index 22b6a303a99..96ae8444f19 100644 --- a/apps/webapp/app/services/dashboardAgentWatchInvestigate.server.ts +++ b/apps/webapp/app/services/dashboardAgentWatchInvestigate.server.ts @@ -30,7 +30,7 @@ export function watchWantsInvestigation(watch: Watch): boolean { } /** The action the agent receives. Stable id, so a retried kick is a no-op. */ -export function watchInvestigateAction(watch: Watch): WatchInvestigateAction { +function watchInvestigateAction(watch: Watch): WatchInvestigateAction { return { type: "watch.investigate" as const, id: `watch:${watch.id}:${watch.status}:investigate`, diff --git a/apps/webapp/app/services/dashboardAgentWatchRunChecks.ts b/apps/webapp/app/services/dashboardAgentWatchRunChecks.ts index ceccbb43f0f..fca3ba53652 100644 --- a/apps/webapp/app/services/dashboardAgentWatchRunChecks.ts +++ b/apps/webapp/app/services/dashboardAgentWatchRunChecks.ts @@ -35,18 +35,18 @@ const FINAL_STATUSES = new Set([ */ const STALE_QUEUED_AT_STATUSES = new Set(["WAITING_TO_RESUME", "RETRYING_AFTER_FAILURE", "PAUSED"]); -export function isTerminalRunStatus(status: string): boolean { +function isTerminalRunStatus(status: string): boolean { return FINAL_STATUSES.has(status); } /** Which timestamp a wait was measured from. */ -export type WatchWaitBasis = "queued_at" | "delay_until" | "created_at"; +type WatchWaitBasis = "queued_at" | "delay_until" | "created_at"; /** * The wait a run has accumulated, labelled with what the data supports. A resumed, retried or * paused run's stale `queuedAt` is never measured from. */ -export function describeRunWait( +function describeRunWait( run: WatchRunRow, now: Date ): { diff --git a/apps/webapp/app/services/dashboardAgentWatchSweep.server.ts b/apps/webapp/app/services/dashboardAgentWatchSweep.server.ts index c975e3d2793..c0b9219aab0 100644 --- a/apps/webapp/app/services/dashboardAgentWatchSweep.server.ts +++ b/apps/webapp/app/services/dashboardAgentWatchSweep.server.ts @@ -62,7 +62,7 @@ const SWEEP_BATCH_LIMIT = 100; const SWEEP_CONCURRENCY = 8; /** What one finalization did. */ -export type WatchFinalizeOutcome = +type WatchFinalizeOutcome = | "fired" | "expired" /** The user lost access: cancelled, and deliberately not narrated. */ @@ -220,7 +220,7 @@ function resolutionFor( * Finalize one overdue watch. Re-authorization comes first, before the final check reads * anything; `canDeliver: false` stops at the resolution, leaving the wake owed. */ -export async function finalizeOverdueWatch( +async function finalizeOverdueWatch( watch: Watch, deps: WatchSweepDeps & { canDeliver?: boolean } = {} ): Promise { @@ -296,7 +296,7 @@ export async function finalizeOverdueWatch( * Recover one owed wake, unconditionally: this sweep can't tell whether the user was already * told. Whether the wake needs prose is decided where the transcript can be read. */ -export async function recoverWatchDelivery(watch: Watch, deps: WatchSweepDeps = {}): Promise { +async function recoverWatchDelivery(watch: Watch, deps: WatchSweepDeps = {}): Promise { const deliver = deps.deliver ?? scheduleWatchDelivery; await deliver(watch); } diff --git a/apps/webapp/app/services/dashboardAgentWatchToken.server.ts b/apps/webapp/app/services/dashboardAgentWatchToken.server.ts index 194bf4e8f5e..f26e7035a5b 100644 --- a/apps/webapp/app/services/dashboardAgentWatchToken.server.ts +++ b/apps/webapp/app/services/dashboardAgentWatchToken.server.ts @@ -90,7 +90,7 @@ export function verifyWatchTokenFromRequest(token: string): Promise { diff --git a/apps/webapp/app/services/dashboardAgentWatches.server.ts b/apps/webapp/app/services/dashboardAgentWatches.server.ts index 06973d58e54..ceb5309304e 100644 --- a/apps/webapp/app/services/dashboardAgentWatches.server.ts +++ b/apps/webapp/app/services/dashboardAgentWatches.server.ts @@ -8,7 +8,6 @@ import { appendChatMessageOnce, armWatchBatch, cancelWatch, - chatExists, claimWatchSubmission, countActiveWatchesForOrg, createChat, @@ -82,9 +81,7 @@ import { import { canAccessDashboardAgent } from "~/v3/canAccessDashboardAgent.server"; /** The task that polls a watch. Lives in the agent project, triggered by us. */ -export const WATCH_TASK_ID = "dashboard-agent-watch"; - -export { MAX_ACTIVE_WATCHES_PER_CHAT }; +const WATCH_TASK_ID = "dashboard-agent-watch"; export type WatchAuthorization = | { ok: true; environment: AuthenticatedEnvironment } @@ -170,7 +167,7 @@ export async function authorizeWatchEnvironmentById(params: { return authorization.ok ? authorization.environment : null; } -export type CreateWatchErrorCode = +type CreateWatchErrorCode = | "limit_reached" | "watch_limit_reached" | "duplicate" @@ -1062,7 +1059,7 @@ export async function scheduleWatchTick(params: { } /** The task that polls a whole (environment, cadence) group. */ -export const WATCH_BATCH_TASK_ID = "dashboard-agent-watch-batch"; +const WATCH_BATCH_TASK_ID = "dashboard-agent-watch-batch"; /** * How long a chain may go silent before it is treated as dead and re-armed. Three cadences @@ -1283,14 +1280,6 @@ export async function listActiveWatchesForChats(params: { ); } -export function chatBelongsToUser(params: { - chatId: string; - userId: string; - organizationId: string; -}): Promise { - return chatExists(dashboardAgentDb, params); -} - export type { ChatWatchContext }; /** diff --git a/apps/webapp/app/services/dashboardPreferences.server.ts b/apps/webapp/app/services/dashboardPreferences.server.ts index 772ba338ac6..6916f1d6c4e 100644 --- a/apps/webapp/app/services/dashboardPreferences.server.ts +++ b/apps/webapp/app/services/dashboardPreferences.server.ts @@ -8,17 +8,13 @@ import { SideMenuPreferences, } from "~/utils/dashboardPreferences"; -export type { - DashboardPreferences, - FavoritePage, - SideMenuPreferences, -} from "~/utils/dashboardPreferences"; +export type { DashboardPreferences, FavoritePage } from "~/utils/dashboardPreferences"; import { type SideMenuSectionId } from "~/components/navigation/sideMenuTypes"; export type { SideMenuSectionId }; import { type ThemePreference } from "~/utils/themePreference"; -export { normalizeThemePreference, type ThemePreference } from "~/utils/themePreference"; +export { type ThemePreference } from "~/utils/themePreference"; export function getDashboardPreferences(data?: any | null): DashboardPreferences { return parseDashboardPreferences(data, (error) => { @@ -437,15 +433,6 @@ export async function updateSideMenuCustomization({ }); } -/** Get the stored item order for a specific list within an organization */ -export function getItemOrder( - sideMenu: SideMenuPreferences | undefined, - organizationId: string, - listId: string -): string[] | undefined { - return sideMenu?.organizations?.[organizationId]?.orderedItems?.[listId]; -} - export async function updateItemOrder({ user, organizationId, diff --git a/apps/webapp/app/services/dataStores/organizationDataStoreConfigSchemas.server.ts b/apps/webapp/app/services/dataStores/organizationDataStoreConfigSchemas.server.ts index a4a2491a2d6..91a9250422a 100644 --- a/apps/webapp/app/services/dataStores/organizationDataStoreConfigSchemas.server.ts +++ b/apps/webapp/app/services/dataStores/organizationDataStoreConfigSchemas.server.ts @@ -5,7 +5,7 @@ import { z } from "zod"; // --------------------------------------------------------------------------- /** V1: single secret-store key that supplies the ClickHouse connection URL. */ -export const ClickhouseDataStoreConfigV1 = z.object({ +const ClickhouseDataStoreConfigV1 = z.object({ version: z.literal(1), data: z.object({ /** Key into the SecretStore that resolves to a ClickhouseConnection ({url}). */ @@ -13,7 +13,7 @@ export const ClickhouseDataStoreConfigV1 = z.object({ }), }); -export type ClickhouseDataStoreConfigV1 = z.infer; +type ClickhouseDataStoreConfigV1 = z.infer; /** Discriminated union over version — extend by adding new literals here. */ export const ClickhouseDataStoreConfig = z.discriminatedUnion("version", [ @@ -30,7 +30,7 @@ export type ClickhouseDataStoreConfig = z.infer): P } } -export async function sendPlainTextEmail(options: SendPlainTextOptions) { - return client.sendPlainText(options); -} - export async function sendEmail(data: DeliverEmail) { return client.send(data); } diff --git a/apps/webapp/app/services/environmentMetricsRepository.server.ts b/apps/webapp/app/services/environmentMetricsRepository.server.ts index 5ecdf13f20f..4dde2008e6c 100644 --- a/apps/webapp/app/services/environmentMetricsRepository.server.ts +++ b/apps/webapp/app/services/environmentMetricsRepository.server.ts @@ -4,7 +4,7 @@ import { QUEUED_STATUSES } from "~/components/runs/v3/TaskRunStatus"; export type CurrentRunningStats = Record; -export interface EnvironmentMetricsRepository { +interface EnvironmentMetricsRepository { getCurrentRunningStats(options: { organizationId: string; projectId: string; diff --git a/apps/webapp/app/services/environmentVariableApiAccess.server.ts b/apps/webapp/app/services/environmentVariableApiAccess.server.ts index 47e769d7b69..16cebec6b43 100644 --- a/apps/webapp/app/services/environmentVariableApiAccess.server.ts +++ b/apps/webapp/app/services/environmentVariableApiAccess.server.ts @@ -48,7 +48,7 @@ type BootstrapAuthenticationDependencies = { authenticateApiKeyRequest: typeof authenticateApiKeyRequest; }; -export async function authenticateEnvironmentScopedApiRequest( +async function authenticateEnvironmentScopedApiRequest( request: Request, action: "read" | "write", resource: EnvironmentScopedResource, diff --git a/apps/webapp/app/services/impersonation.server.ts b/apps/webapp/app/services/impersonation.server.ts index e69a3fb305b..aa850ba0468 100644 --- a/apps/webapp/app/services/impersonation.server.ts +++ b/apps/webapp/app/services/impersonation.server.ts @@ -7,7 +7,7 @@ import { env } from "~/env.server"; import { logger } from "~/services/logger.server"; import { resolveImpersonationState, type ImpersonationState } from "~/utils/impersonationState"; -export const impersonationSessionStorage = createCookieSessionStorage({ +const impersonationSessionStorage = createCookieSessionStorage({ cookie: { name: "__impersonate", // use any name you want here sameSite: "lax", // this helps with CSRF @@ -28,7 +28,7 @@ const IMPERSONATED_USER_ID_KEY = "impersonatedUserId"; */ const VIEWING_AS_USER_KEY = "viewingAsUser"; -export function getImpersonationSession(request: Request) { +function getImpersonationSession(request: Request) { return impersonationSessionStorage.getSession(request.headers.get("Cookie")); } diff --git a/apps/webapp/app/services/lastAuthMethod.server.ts b/apps/webapp/app/services/lastAuthMethod.server.ts index 6fdbc80917c..670f03d12bf 100644 --- a/apps/webapp/app/services/lastAuthMethod.server.ts +++ b/apps/webapp/app/services/lastAuthMethod.server.ts @@ -4,7 +4,7 @@ import { env } from "~/env.server"; export type LastAuthMethod = "github" | "google" | "email" | "sso"; // Cookie that persists for 1 year to remember the user's last login method -export const lastAuthMethodCookie = createCookie("last-auth-method", { +const lastAuthMethodCookie = createCookie("last-auth-method", { maxAge: 60 * 60 * 24 * 365, // 1 year httpOnly: true, sameSite: "lax", diff --git a/apps/webapp/app/services/logger.server.ts b/apps/webapp/app/services/logger.server.ts index a31c7cc5a18..4d5efe49dd0 100644 --- a/apps/webapp/app/services/logger.server.ts +++ b/apps/webapp/app/services/logger.server.ts @@ -8,10 +8,6 @@ import { captureException, captureMessage } from "@sentry/remix"; const currentFieldsStore = new AsyncLocalStorage>(); -export function trace(fields: Record, fn: () => T): T { - return currentFieldsStore.run(fields, fn); -} - // The keys below aren't already in the Logger's default deny-list. Passing them here means the // extra data sent to Sentry gets the same redaction as the stdout line, instead of bypassing it. const SENTRY_EXTRA_FILTERED_KEYS = ["examples", "connectionString"]; @@ -75,28 +71,6 @@ export const logger = new Logger( } ); -export const workerLogger = new Logger( - "worker", - (process.env.APP_LOG_LEVEL ?? "info") as LogLevel, - ["examples", "output", "connectionString"], - sensitiveDataReplacer, - () => { - const fields = currentFieldsStore.getStore(); - return fields ? { ...fields } : {}; - } -); - -export const socketLogger = new Logger( - "socket", - (process.env.APP_LOG_LEVEL ?? "info") as LogLevel, - [], - sensitiveDataReplacer, - () => { - const fields = currentFieldsStore.getStore(); - return fields ? { ...fields } : {}; - } -); - // Opt-in, dev-only: mirror this process's stdout to a local telnet/TCP stream. // We patch console (rather than the static Logger.onLog sink) so the stream also captures logs // from separate/bundled copies of the Logger — e.g. the enterprise SSO plugin, which bundles its diff --git a/apps/webapp/app/services/mfa/mfaRateLimiterGlobal.server.ts b/apps/webapp/app/services/mfa/mfaRateLimiterGlobal.server.ts index ff5e47c7864..ceb6b42db43 100644 --- a/apps/webapp/app/services/mfa/mfaRateLimiterGlobal.server.ts +++ b/apps/webapp/app/services/mfa/mfaRateLimiterGlobal.server.ts @@ -22,9 +22,6 @@ const mfaRateLimiters = singleton("mfaRateLimiters", () => }) ); -export const mfaRateLimiter = mfaRateLimiters.perMinute; -export const mfaDailyRateLimiter = mfaRateLimiters.daily; - /** * Production entrypoint: rate-limit an MFA validation attempt for `userId` * against the env-configured limiter pair. Throws `MfaRateLimitError` when diff --git a/apps/webapp/app/services/onboardingSession.server.ts b/apps/webapp/app/services/onboardingSession.server.ts deleted file mode 100644 index 0e166d05487..00000000000 --- a/apps/webapp/app/services/onboardingSession.server.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Session } from "@remix-run/node"; -import { createCookieSessionStorage } from "@remix-run/node"; -import { env } from "~/env.server"; - -export const onboardingSessionStorage = createCookieSessionStorage({ - cookie: { - name: "__onboarding", // use any name you want here - sameSite: "lax", // this helps with CSRF - path: "/", // remember to add this so the cookie will work in all routes - httpOnly: true, // for security reasons, make this cookie http only - secrets: [env.SESSION_SECRET], - secure: env.NODE_ENV === "production", // enable this in prod only - maxAge: 60 * 60 * 24, // 1 day - }, -}); - -export function getOnboardingSession(request: Request) { - return onboardingSessionStorage.getSession(request.headers.get("Cookie")); -} - -export function commitOnboardingSession(session: Session) { - return onboardingSessionStorage.commitSession(session); -} - -export async function getWorkflowDate(request: Request) { - const session = await getOnboardingSession(request); - - const rawWorkflowDate = session.get("workflowDate"); - - if (rawWorkflowDate) { - return new Date(rawWorkflowDate); - } -} - -export async function setWorkflowDate(date: Date, request: Request) { - const session = await getOnboardingSession(request); - - session.set("workflowDate", date.toISOString()); - - return session; -} - -export async function clearWorkflowDate(request: Request) { - const session = await getOnboardingSession(request); - - session.unset("workflowDate"); - - return session; -} diff --git a/apps/webapp/app/services/organizationAccessToken.server.ts b/apps/webapp/app/services/organizationAccessToken.server.ts index 77519ef8d1f..fe87c4d611c 100644 --- a/apps/webapp/app/services/organizationAccessToken.server.ts +++ b/apps/webapp/app/services/organizationAccessToken.server.ts @@ -1,65 +1,14 @@ -import { customAlphabet } from "nanoid"; import { z } from "zod"; import { prisma } from "~/db.server"; import { logger } from "./logger.server"; import { hashToken } from "~/utils/tokens.server"; -const tokenValueLength = 40; -//lowercase only, removed 0 and l to avoid confusion -const tokenGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", tokenValueLength); - // Skip the lastAccessedAt write if the existing value is already within this // window. Eliminates per-auth UPDATE churn on a small narrow hot table; the // settings UI reads this field at human granularity so a few-minute // staleness is fine. export const OAT_LAST_ACCESSED_THROTTLE_MS = 5 * 60 * 1000; -type CreateOrganizationAccessTokenOptions = { - name: string; - organizationId: string; - expiresAt?: Date; -}; - -export async function getValidOrganizationAccessTokens(organizationId: string) { - const organizationAccessTokens = await prisma.organizationAccessToken.findMany({ - select: { - id: true, - name: true, - createdAt: true, - lastAccessedAt: true, - expiresAt: true, - }, - where: { - organizationId, - revokedAt: null, - OR: [{ expiresAt: null }, { expiresAt: { gte: new Date() } }], - }, - }); - - return organizationAccessTokens.map((oat) => ({ - id: oat.id, - name: oat.name, - createdAt: oat.createdAt, - lastAccessedAt: oat.lastAccessedAt, - expiresAt: oat.expiresAt, - })); -} - -export type ObfuscatedOrganizationAccessToken = Awaited< - ReturnType ->[number]; - -export async function revokeOrganizationAccessToken(tokenId: string) { - await prisma.organizationAccessToken.update({ - where: { - id: tokenId, - }, - data: { - revokedAt: new Date(), - }, - }); -} - export type OrganizationAccessTokenAuthenticationResult = { organizationId: string; }; @@ -139,37 +88,4 @@ export function isOrganizationAccessToken(token: string) { return token.startsWith(tokenPrefix); } -export async function createOrganizationAccessToken({ - name, - organizationId, - expiresAt, -}: CreateOrganizationAccessTokenOptions) { - const token = createToken(); - - const organizationAccessToken = await prisma.organizationAccessToken.create({ - data: { - name, - organizationId, - hashedToken: hashToken(token), - expiresAt, - }, - }); - - return { - id: organizationAccessToken.id, - name, - organizationId, - token, - expiresAt: organizationAccessToken.expiresAt, - }; -} - -export type CreatedOrganizationAccessToken = Awaited< - ReturnType ->; - const tokenPrefix = "tr_oat_"; - -function createToken() { - return `${tokenPrefix}${tokenGenerator()}`; -} diff --git a/apps/webapp/app/services/platformNotifications.server.ts b/apps/webapp/app/services/platformNotifications.server.ts index 0b39fd915c1..0684dda7efa 100644 --- a/apps/webapp/app/services/platformNotifications.server.ts +++ b/apps/webapp/app/services/platformNotifications.server.ts @@ -15,10 +15,7 @@ import { } from "./platformNotificationSchemas"; import { isCliVersionEligible } from "./platformNotificationVersionTargeting"; -export { - CreatePlatformNotificationSchema, - UpdatePlatformNotificationSchema, -} from "./platformNotificationSchemas"; +export { UpdatePlatformNotificationSchema } from "./platformNotificationSchemas"; export type { CreatePlatformNotificationInput, PayloadV1 } from "./platformNotificationSchemas"; export type PlatformNotificationWithPayload = { diff --git a/apps/webapp/app/services/preferences/uiPreferences.server.ts b/apps/webapp/app/services/preferences/uiPreferences.server.ts index 44282499db3..74ad51939ef 100644 --- a/apps/webapp/app/services/preferences/uiPreferences.server.ts +++ b/apps/webapp/app/services/preferences/uiPreferences.server.ts @@ -13,7 +13,7 @@ export const uiPreferencesStorage = createCookieSessionStorage({ }, }); -export function getUiPreferencesSession(request: Request) { +function getUiPreferencesSession(request: Request) { return uiPreferencesStorage.getSession(request.headers.get("Cookie")); } diff --git a/apps/webapp/app/services/promoCode.server.ts b/apps/webapp/app/services/promoCode.server.ts index af8f5424a74..c720818b1f1 100644 --- a/apps/webapp/app/services/promoCode.server.ts +++ b/apps/webapp/app/services/promoCode.server.ts @@ -4,7 +4,7 @@ import { env } from "~/env.server"; // Carries a promo code from the landing page through signup to first-org // creation. httpOnly + sameSite=lax so it survives the OAuth round-trip, // matching the existing redirect-to cookie. -export const promoCodeCookie = createCookie("promo-code", { +const promoCodeCookie = createCookie("promo-code", { maxAge: 60 * 60, // 1 hour — enough to complete signup httpOnly: true, sameSite: "lax", diff --git a/apps/webapp/app/services/publicTokens.server.ts b/apps/webapp/app/services/publicTokens.server.ts index 590a7f3b2c9..8d471a5b7c7 100644 --- a/apps/webapp/app/services/publicTokens.server.ts +++ b/apps/webapp/app/services/publicTokens.server.ts @@ -7,7 +7,7 @@ import { apiKeyTelemetry, type ApiKeyTelemetry } from "~/services/apiKeyTelemetr import { rbac } from "~/services/rbac.server"; // Public access tokens may be valid for at most 30 days. -export const MAX_PUBLIC_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60; +const MAX_PUBLIC_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60; const RequestBodySchema = z.object({ scopes: z.array(z.string()).min(1), diff --git a/apps/webapp/app/services/queryService.server.ts b/apps/webapp/app/services/queryService.server.ts index f4bf4b940a4..0b81d93e76c 100644 --- a/apps/webapp/app/services/queryService.server.ts +++ b/apps/webapp/app/services/queryService.server.ts @@ -26,10 +26,8 @@ import { import { getLimit } from "./platform.v3.server"; import { timeFilters, timeFilterFromTo } from "~/components/runs/v3/SharedFilters"; import parse from "parse-duration"; -import { querySchemas, QueryScopeSchema, type QueryScope } from "~/v3/querySchemas"; - -export { QueryScopeSchema }; -export type { TableSchema, TSQLQueryResult, QueryScope }; +import { querySchemas, type QueryScope } from "~/v3/querySchemas"; +export type { TSQLQueryResult, QueryScope }; const scopeToEnum = { organization: "ORGANIZATION", diff --git a/apps/webapp/app/services/rateLimiter.server.ts b/apps/webapp/app/services/rateLimiter.server.ts index 499892e4003..b37ce65175a 100644 --- a/apps/webapp/app/services/rateLimiter.server.ts +++ b/apps/webapp/app/services/rateLimiter.server.ts @@ -6,13 +6,7 @@ import { type RateLimiterRedisClient, } from "./rateLimiterCore.server"; -export { - createRedisRateLimitClient, - type Duration, - type Limiter, - type RateLimitResponse, - type RateLimiterRedisClient, -} from "./rateLimiterCore.server"; +export { createRedisRateLimitClient, type Duration, type Limiter } from "./rateLimiterCore.server"; type Options = { redis?: RedisWithClusterOptions; diff --git a/apps/webapp/app/services/realtime/electricStreamProtocol.server.ts b/apps/webapp/app/services/realtime/electricStreamProtocol.server.ts index fc3a3285af9..491825c46d5 100644 --- a/apps/webapp/app/services/realtime/electricStreamProtocol.server.ts +++ b/apps/webapp/app/services/realtime/electricStreamProtocol.server.ts @@ -154,7 +154,7 @@ function serializeValue(value: unknown, column: ElectricColumn): string | null { } /** The merge key the client uses to reassemble a row across insert/update cycles. */ -export function runShapeKey(runId: string): string { +function runShapeKey(runId: string): string { return `"public"."TaskRun"/"${runId}"`; } diff --git a/apps/webapp/app/services/realtime/envChangeRouter.server.ts b/apps/webapp/app/services/realtime/envChangeRouter.server.ts index e183bf636a3..6446de94b31 100644 --- a/apps/webapp/app/services/realtime/envChangeRouter.server.ts +++ b/apps/webapp/app/services/realtime/envChangeRouter.server.ts @@ -9,7 +9,7 @@ import { logger } from "~/services/logger.server"; * serializes each row's wire value once, and resolves each matched feed's pending wait. Stateless across reconnects. */ -export type WakeReason = "notify" | "timeout" | "abort"; +type WakeReason = "notify" | "timeout" | "abort"; /** A feed's membership predicate over the env stream. */ export type FeedFilter = @@ -21,7 +21,7 @@ export type FeedFilter = * its wire `value` serialized once for this feed's column set (shared across feeds). */ export type MatchedRow = { row: RealtimeRunRow; value: Record }; -export type WaitResult = { reason: WakeReason; rows: MatchedRow[] }; +type WaitResult = { reason: WakeReason; rows: MatchedRow[] }; /** Minimal deps so the router is unit-testable without Redis/Postgres. */ export interface EnvChangeSource { @@ -64,7 +64,7 @@ export type EnvChangeRouterOptions = { replicaLag?: ReplicaLagGate; }; -export type ReplicaLagGate = { +type ReplicaLagGate = { /** Current replica-lag estimate (ms). */ getLagMs(): number; /** Feedback: a hydrate provably read at least this far behind the primary. */ diff --git a/apps/webapp/app/services/realtime/jwtAuth.server.ts b/apps/webapp/app/services/realtime/jwtAuth.server.ts index 2806a737017..8f40c706c56 100644 --- a/apps/webapp/app/services/realtime/jwtAuth.server.ts +++ b/apps/webapp/app/services/realtime/jwtAuth.server.ts @@ -9,13 +9,13 @@ import { $replica } from "~/db.server"; import { findEnvironmentById } from "~/models/runtimeEnvironment.server"; import type { AuthenticatedEnvironment } from "../apiAuth.server"; -export type ValidatePublicJwtKeySuccess = { +type ValidatePublicJwtKeySuccess = { ok: true; environment: AuthenticatedEnvironment; claims: Record; }; -export type ValidatePublicJwtKeyError = { +type ValidatePublicJwtKeyError = { ok: false; error: string; }; diff --git a/apps/webapp/app/services/realtime/mintRunToken.server.ts b/apps/webapp/app/services/realtime/mintRunToken.server.ts deleted file mode 100644 index 2cdc4316e66..00000000000 --- a/apps/webapp/app/services/realtime/mintRunToken.server.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { generateJWT as internal_generateJWT } from "@trigger.dev/core/v3"; -import { extractJwtSigningSecretKey } from "./jwtAuth.server"; - -type Environment = Parameters[0]; - -export type MintRunTokenOptions = { - /** Include the input-stream write scope (needed for steering messages from the playground). */ - includeInputStreamWrite?: boolean; - /** Token expiration. Defaults to "1h". */ - expirationTime?: string; -}; - -/** - * Mint a run-scoped public access token (JWT) for browser subscription to a - * run's realtime streams. - * - * Used by: - * - The playground action to give a freshly triggered chat session a token. - * - The run details page to let the agent view subscribe to the chat stream - * of an existing run (read-only). - */ -export async function mintRunToken( - environment: Environment, - runFriendlyId: string, - options: MintRunTokenOptions = {} -): Promise { - const scopes = [`read:runs:${runFriendlyId}`]; - if (options.includeInputStreamWrite) { - scopes.push(`write:inputStreams:${runFriendlyId}`); - } - - return internal_generateJWT({ - secretKey: extractJwtSigningSecretKey(environment), - payload: { - sub: environment.id, - pub: true, - scopes, - }, - expirationTime: options.expirationTime ?? "1h", - }); -} diff --git a/apps/webapp/app/services/realtime/nativeRealtimeClient.server.ts b/apps/webapp/app/services/realtime/nativeRealtimeClient.server.ts index 5d740a24e23..981c4d83188 100644 --- a/apps/webapp/app/services/realtime/nativeRealtimeClient.server.ts +++ b/apps/webapp/app/services/realtime/nativeRealtimeClient.server.ts @@ -66,11 +66,11 @@ export interface RealtimeStreamClient { ): Promise; } -export type WakeupReason = "notify" | "timeout" | "abort"; +type WakeupReason = "notify" | "timeout" | "abort"; /** How a live poll resolved: `fast-hydrate` (router woke us, hydrate-by-id), `full-resolve` * (backstop), or `cold-resolve` (fresh env subscription probed once instead of holding blind). */ -export type LivePollPath = "fast-hydrate" | "full-resolve" | "cold-resolve"; +type LivePollPath = "fast-hydrate" | "full-resolve" | "cold-resolve"; export type NativeRealtimeClientOptions = { runReader: RunHydrator; diff --git a/apps/webapp/app/services/realtime/replicaLagEstimator.server.ts b/apps/webapp/app/services/realtime/replicaLagEstimator.server.ts index d077ea32439..540ac8544ef 100644 --- a/apps/webapp/app/services/realtime/replicaLagEstimator.server.ts +++ b/apps/webapp/app/services/realtime/replicaLagEstimator.server.ts @@ -59,7 +59,7 @@ export class AuroraReplicaLagSource implements ReplicaLagSource { * low-traffic systems, which (measured locally) pins the estimate at the delay cap — so * mid-apply reports undefined and the tripwire's observed-staleness floor carries the * estimate instead. */ -export class VanillaPgReplicaLagSource implements ReplicaLagSource { +class VanillaPgReplicaLagSource implements ReplicaLagSource { readonly name = "vanilla-pg"; constructor(private readonly db: RawQueryable) {} diff --git a/apps/webapp/app/services/realtime/runChangeNotifier.server.ts b/apps/webapp/app/services/realtime/runChangeNotifier.server.ts index 66bbb3120e4..709f9715940 100644 --- a/apps/webapp/app/services/realtime/runChangeNotifier.server.ts +++ b/apps/webapp/app/services/realtime/runChangeNotifier.server.ts @@ -2,7 +2,7 @@ import type { RedisClient, RedisWithClusterOptions } from "~/redis.server"; import { createRedisClient } from "~/redis.server"; import { logger } from "../logger.server"; -export const CHANGE_RECORD_VERSION = 1; +const CHANGE_RECORD_VERSION = 1; /** * A self-describing run-change fact published once to the run's environment channel; row state is diff --git a/apps/webapp/app/services/realtime/runChangeNotifierInstance.server.ts b/apps/webapp/app/services/realtime/runChangeNotifierInstance.server.ts index 2e032bd1599..9e718af3151 100644 --- a/apps/webapp/app/services/realtime/runChangeNotifierInstance.server.ts +++ b/apps/webapp/app/services/realtime/runChangeNotifierInstance.server.ts @@ -65,11 +65,6 @@ export function getRunChangeNotifier(): RunChangeNotifier { return singleton("runChangeNotifier", initializeRunChangeNotifier); } -/** Whether the notifier subsystem is enabled for this process. */ -export function isRunChangeNotifierEnabled(): boolean { - return nativeBackendEnabled; -} - /** Fire-and-forget publish of a run-changed record. No-op (and no notifier construction) * when disabled, so publish sites can call it unconditionally. */ export function publishChangeRecord(input: ChangeRecordInput): void { @@ -84,16 +79,3 @@ export function publishChangeRecord(input: ChangeRecordInput): void { logger.error("[runChangeNotifier] publishChangeRecord threw; dropping notification", { error }); } } - -export function publishManyChangeRecords(inputs: ChangeRecordInput[]): void { - if (!nativeBackendEnabled) { - return; - } - try { - getRunChangeNotifier().publishMany(inputs); - } catch (error) { - logger.error("[runChangeNotifier] publishManyChangeRecords threw; dropping notifications", { - error, - }); - } -} diff --git a/apps/webapp/app/services/realtime/runReader.server.ts b/apps/webapp/app/services/realtime/runReader.server.ts index 4308e3a7f14..11c861c8ed4 100644 --- a/apps/webapp/app/services/realtime/runReader.server.ts +++ b/apps/webapp/app/services/realtime/runReader.server.ts @@ -15,7 +15,7 @@ import { RESERVED_COLUMNS, type RealtimeRunRow } from "./electricStreamProtocol. */ /** The TaskRun columns the realtime feed projects (mirrors DEFAULT_ELECTRIC_COLUMNS). */ -export const RUN_HYDRATOR_SELECT = { +const RUN_HYDRATOR_SELECT = { id: true, taskIdentifier: true, createdAt: true, diff --git a/apps/webapp/app/services/realtime/s2realtimeStreams.server.ts b/apps/webapp/app/services/realtime/s2realtimeStreams.server.ts index aff543d3052..e3485a63ebd 100644 --- a/apps/webapp/app/services/realtime/s2realtimeStreams.server.ts +++ b/apps/webapp/app/services/realtime/s2realtimeStreams.server.ts @@ -19,14 +19,14 @@ import { ServiceValidationError } from "~/v3/services/common.server"; // // We attach no record headers (H=0), so the budget reduces to: // 8 + body ≤ 1048576 → body ≤ 1048568 -export const S2_MAX_METERED_BYTES = 1024 * 1024; // 1 MiB -export const S2_RECORD_BASE_OVERHEAD_BYTES = 8; +const S2_MAX_METERED_BYTES = 1024 * 1024; // 1 MiB +const S2_RECORD_BASE_OVERHEAD_BYTES = 8; /** * Thrown when a record's metered size would exceed S2's hard per-record * limit. Caught by the route handler and surfaced as 413. */ -export class S2RecordTooLargeError extends ServiceValidationError { +class S2RecordTooLargeError extends ServiceValidationError { constructor(public readonly meteredBytes: number) { super( `Record metered size ${meteredBytes} bytes exceeds the S2 per-record limit of ${S2_MAX_METERED_BYTES} bytes. Reduce tool-output size or split into smaller parts.`, diff --git a/apps/webapp/app/services/realtime/sessionRunManager.server.ts b/apps/webapp/app/services/realtime/sessionRunManager.server.ts index 53d436e1e57..a1989a9ef7a 100644 --- a/apps/webapp/app/services/realtime/sessionRunManager.server.ts +++ b/apps/webapp/app/services/realtime/sessionRunManager.server.ts @@ -21,11 +21,11 @@ import { determineRealtimeStreamsVersion } from "./v1StreamsGlobal.server"; * an `isContinuation` flag) come in via the `payloadOverrides` argument * to `ensureRunForSession` and shallow-merge on top of `basePayload`. */ -export const SessionTriggerConfigSchema = SessionTriggerConfigZod; +const SessionTriggerConfigSchema = SessionTriggerConfigZod; export type SessionTriggerConfig = z.infer; -export type EnsureRunReason = "initial" | "continuation" | "upgrade" | "manual"; +type EnsureRunReason = "initial" | "continuation" | "upgrade" | "manual"; /** * Hard cap on how many times `ensureRunForSession` will recurse on the @@ -533,6 +533,6 @@ async function cancelLostRaceRun( await service.call(run, { reason: "Lost session-run claim race" }); } -export class SessionRunManagerError extends Error { +class SessionRunManagerError extends Error { readonly name = "SessionRunManagerError"; } diff --git a/apps/webapp/app/services/realtime/shadowCompare.server.ts b/apps/webapp/app/services/realtime/shadowCompare.server.ts index 27831dd68a2..abc723ff421 100644 --- a/apps/webapp/app/services/realtime/shadowCompare.server.ts +++ b/apps/webapp/app/services/realtime/shadowCompare.server.ts @@ -24,7 +24,7 @@ type ShapeMessage = { const COLUMN_BY_NAME = new Map(RUN_ELECTRIC_COLUMNS.map((column) => [column.name, column])); -export type ColumnDiff = { +type ColumnDiff = { runId: string; column: string; electric: string | null; diff --git a/apps/webapp/app/services/realtime/streamBasinProvisioner.server.ts b/apps/webapp/app/services/realtime/streamBasinProvisioner.server.ts index 92c07104a8f..f1bb9fc346f 100644 --- a/apps/webapp/app/services/realtime/streamBasinProvisioner.server.ts +++ b/apps/webapp/app/services/realtime/streamBasinProvisioner.server.ts @@ -14,18 +14,18 @@ import { logger } from "~/services/logger.server"; import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server"; import { parseDuration } from "./duration.server"; -export function isPerOrgBasinsEnabled(): boolean { +function isPerOrgBasinsEnabled(): boolean { return env.REALTIME_STREAMS_PER_ORG_BASINS_ENABLED === "true"; } -export function defaultRetention(): string { +function defaultRetention(): string { return env.REALTIME_STREAMS_BASIN_DEFAULT_RETENTION; } // Org id is a cuid — fixed-length and stable, so the basin name is // collision-free without truncation. Slugs are user-editable and would // drift. -export function basinNameForOrg(org: { id: string }): string { +function basinNameForOrg(org: { id: string }): string { const prefix = env.REALTIME_STREAMS_BASIN_NAME_PREFIX; const envName = env.REALTIME_STREAMS_BASIN_NAME_ENV; return `${prefix}-${envName}-org-${org.id}`; @@ -43,7 +43,7 @@ type ProvisionResult = // Idempotent. Treats S2 409 as success (race with another caller, or // previous run that crashed after S2 ack but before the column write). -export async function provisionBasinForOrg( +async function provisionBasinForOrg( org: ProvisionInput, prismaClient: PrismaClientOrTransaction = prisma ): Promise { @@ -89,7 +89,7 @@ export async function provisionBasinForOrg( return { kind: "provisioned", basin, retention }; } -export async function reconfigureBasinForOrg(orgId: string, retention: string): Promise { +async function reconfigureBasinForOrg(orgId: string, retention: string): Promise { if (!isPerOrgBasinsEnabled()) return; const accessToken = env.REALTIME_STREAMS_S2_ACCESS_TOKEN; diff --git a/apps/webapp/app/services/realtime/utils.server.ts b/apps/webapp/app/services/realtime/utils.server.ts deleted file mode 100644 index 9655878fe89..00000000000 --- a/apps/webapp/app/services/realtime/utils.server.ts +++ /dev/null @@ -1,33 +0,0 @@ -export class LineTransformStream extends TransformStream { - private buffer = ""; - - constructor() { - super({ - transform: (chunk, controller) => { - // Append the chunk to the buffer - this.buffer += chunk; - - // Split on newlines - const lines = this.buffer.split("\n"); - - // The last element might be incomplete, hold it back in buffer - this.buffer = lines.pop() || ""; - - // Filter out empty or whitespace-only lines - const fullLines = lines.filter((line) => line.trim().length > 0); - - // If we got any complete lines, emit them as an array - if (fullLines.length > 0) { - controller.enqueue(fullLines); - } - }, - flush: (controller) => { - // On stream end, if there's leftover text, emit it as a single-element array - const trimmed = this.buffer.trim(); - if (trimmed.length > 0) { - controller.enqueue([trimmed]); - } - }, - }); - } -} diff --git a/apps/webapp/app/services/realtime/v1StreamsGlobal.server.ts b/apps/webapp/app/services/realtime/v1StreamsGlobal.server.ts index 27305f676e9..4ba7cde68c0 100644 --- a/apps/webapp/app/services/realtime/v1StreamsGlobal.server.ts +++ b/apps/webapp/app/services/realtime/v1StreamsGlobal.server.ts @@ -10,10 +10,7 @@ import { singleton } from "~/utils/singleton"; import type { AuthenticatedEnvironment } from "../apiAuth.server"; import { RedisRealtimeStreams } from "./redisRealtimeStreams.server"; import { S2RealtimeStreams } from "./s2realtimeStreams.server"; -import { - resolveRealtimeStreamsVersion, - type RealtimeStreamsVersionConfig, -} from "./realtimeStreamsVersion"; +import { resolveRealtimeStreamsVersion } from "./realtimeStreamsVersion"; import type { StreamIngestor, StreamResponder } from "./types"; function initializeRedisRealtimeStreams() { @@ -31,7 +28,7 @@ function initializeRedisRealtimeStreams() { }); } -export const v1RealtimeStreams = singleton("realtimeStreams", initializeRedisRealtimeStreams); +const v1RealtimeStreams = singleton("realtimeStreams", initializeRedisRealtimeStreams); /** * Resolve a stream's basin. Precedence: run → session → org → global env. @@ -100,8 +97,6 @@ function streamPrefixFor(environment: AuthenticatedEnvironment, basin: string): return segments.join("/"); } -export type { RealtimeStreamsVersionConfig }; - /** * Pass `organizationBasinName` wherever the caller has it. It mirrors the * organization step of {@link resolveStreamBasin}, and is what lets a diff --git a/apps/webapp/app/services/redirectTo.server.ts b/apps/webapp/app/services/redirectTo.server.ts index 0b41e24e1b3..69b16898f19 100644 --- a/apps/webapp/app/services/redirectTo.server.ts +++ b/apps/webapp/app/services/redirectTo.server.ts @@ -4,7 +4,7 @@ import { env } from "~/env.server"; const ONE_DAY = 60 * 60 * 24; -export const { commitSession, getSession } = createCookieSessionStorage({ +const redirectToSessionStorage = createCookieSessionStorage({ cookie: { name: "__redirectTo", path: "/", @@ -16,7 +16,10 @@ export const { commitSession, getSession } = createCookieSessionStorage({ }, }); -export function getRedirectSession(request: Request) { +export const { commitSession } = redirectToSessionStorage; +const { getSession } = redirectToSessionStorage; + +function getRedirectSession(request: Request) { return getSession(request.headers.get("Cookie")); } diff --git a/apps/webapp/app/services/referralSource.server.ts b/apps/webapp/app/services/referralSource.server.ts index e98c8ebcb2c..b1e0a11085c 100644 --- a/apps/webapp/app/services/referralSource.server.ts +++ b/apps/webapp/app/services/referralSource.server.ts @@ -9,14 +9,14 @@ const ReferralSourceSchema = z.enum(["vercel"]); export type ReferralSource = z.infer; // Cookie that persists for 1 hour to track referral source during login flow -export const referralSourceCookie = createCookie("referral-source", { +const referralSourceCookie = createCookie("referral-source", { maxAge: 60 * 60, // 1 hour httpOnly: true, sameSite: "lax", secure: env.NODE_ENV === "production", }); -export async function getReferralSource(request: Request): Promise { +async function getReferralSource(request: Request): Promise { const cookie = request.headers.get("Cookie"); const value = await referralSourceCookie.parse(cookie); const parsed = ReferralSourceSchema.safeParse(value); @@ -27,7 +27,7 @@ export async function setReferralSourceCookie(source: ReferralSource): Promise { +async function clearReferralSourceCookie(): Promise { return referralSourceCookie.serialize("", { maxAge: 0, }); diff --git a/apps/webapp/app/services/renderMarkdown.server.ts b/apps/webapp/app/services/renderMarkdown.server.ts deleted file mode 100644 index 2e134109f38..00000000000 --- a/apps/webapp/app/services/renderMarkdown.server.ts +++ /dev/null @@ -1,21 +0,0 @@ -import prism from "prismjs"; -import "prismjs/components/prism-typescript"; -import "prismjs/components/prism-json"; -import "prismjs/components/prism-bash"; -import "prismjs/plugins/line-numbers/prism-line-numbers"; -import "prismjs/plugins/line-numbers/prism-line-numbers.css"; -import { marked } from "marked"; - -export function renderMarkdown(markdown: string) { - const html = marked(markdown, { - highlight: function (code, lang) { - if (prism.languages[lang]) { - return prism.highlight(code, prism.languages[lang], lang); - } - - return code; - }, - }); - - return html; -} diff --git a/apps/webapp/app/services/runsReplicationGlobal.server.ts b/apps/webapp/app/services/runsReplicationGlobal.server.ts index 48e783ef56a..af65685d28e 100644 --- a/apps/webapp/app/services/runsReplicationGlobal.server.ts +++ b/apps/webapp/app/services/runsReplicationGlobal.server.ts @@ -34,15 +34,3 @@ export function getRunsReplicationConfiguredSources(): ConfiguredSource[] | unde export function setRunsReplicationConfiguredSources(sources: ConfiguredSource[]) { _global[GLOBAL_RUNS_REPLICATION_SOURCES_KEY] = sources; } - -export function getTcpMonitorGlobal(): NodeJS.Timeout | undefined { - return _global[GLOBAL_TCP_MONITOR_KEY]; -} - -export function setTcpMonitorGlobal(timeout: NodeJS.Timeout) { - _global[GLOBAL_TCP_MONITOR_KEY] = timeout; -} - -export function unregisterTcpMonitorGlobal() { - delete _global[GLOBAL_TCP_MONITOR_KEY]; -} diff --git a/apps/webapp/app/services/runsRepository/runsRepository.server.ts b/apps/webapp/app/services/runsRepository/runsRepository.server.ts index a349c5bf534..431ed271883 100644 --- a/apps/webapp/app/services/runsRepository/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/runsRepository.server.ts @@ -146,7 +146,7 @@ export type TagList = { tags: string[]; }; -export type CursorPagination = { +type CursorPagination = { nextCursor: string | null; previousCursor: string | null; }; diff --git a/apps/webapp/app/services/secrets/secretStoreOptionsSchema.server.ts b/apps/webapp/app/services/secrets/secretStoreOptionsSchema.server.ts index 7ffc22743dc..ceac7946e56 100644 --- a/apps/webapp/app/services/secrets/secretStoreOptionsSchema.server.ts +++ b/apps/webapp/app/services/secrets/secretStoreOptionsSchema.server.ts @@ -1,4 +1,4 @@ import { z } from "zod"; -export const SecretStoreOptionsSchema = z.enum(["DATABASE", "AWS_PARAM_STORE"]); +const SecretStoreOptionsSchema = z.enum(["DATABASE", "AWS_PARAM_STORE"]); export type SecretStoreOptions = z.infer; diff --git a/apps/webapp/app/services/sensitiveDataReplacer.ts b/apps/webapp/app/services/sensitiveDataReplacer.ts index a66757c7ee4..a8ac468053b 100644 --- a/apps/webapp/app/services/sensitiveDataReplacer.ts +++ b/apps/webapp/app/services/sensitiveDataReplacer.ts @@ -1,12 +1,12 @@ import { z } from "zod"; -export const RedactStringSchema = z.object({ +const RedactStringSchema = z.object({ __redactedString: z.literal(true), strings: z.array(z.string()), interpolations: z.array(z.string()), }); -export type RedactString = z.infer; +type RedactString = z.infer; // Replaces redacted strings with "******". // For example, this object: {"Authorization":{"__redactedString":true,"strings":["Bearer ",""],"interpolations":["sk-1234"]}} diff --git a/apps/webapp/app/services/session.server.ts b/apps/webapp/app/services/session.server.ts index 753bc7f6a17..90cda576a08 100644 --- a/apps/webapp/app/services/session.server.ts +++ b/apps/webapp/app/services/session.server.ts @@ -183,6 +183,6 @@ export function hasAdminDisplayAccess(user: { return (user.admin || user.isImpersonating) && !user.isViewingAsUser; } -export async function logout(request: Request) { +async function logout(request: Request) { return redirect("/logout"); } diff --git a/apps/webapp/app/services/sessionStorage.server.ts b/apps/webapp/app/services/sessionStorage.server.ts index c54561d647b..6fa68f33087 100644 --- a/apps/webapp/app/services/sessionStorage.server.ts +++ b/apps/webapp/app/services/sessionStorage.server.ts @@ -24,4 +24,4 @@ export function getUserSession(request: Request) { return sessionStorage.getSession(request.headers.get("Cookie")); } -export const { getSession, commitSession, destroySession } = sessionStorage; +export const { getSession, commitSession } = sessionStorage; diff --git a/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts b/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts index 4c15d0423b0..fc0a043573b 100644 --- a/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts +++ b/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts @@ -47,10 +47,6 @@ const SessionListInputOptionsSchema = z.object({ }); export type SessionListInputOptions = z.infer; -export type SessionListInputFilters = Omit< - SessionListInputOptions, - "organizationId" | "projectId" | "environmentId" ->; export type FilterSessionsOptions = Omit & { /** period converted to milliseconds duration */ @@ -83,11 +79,11 @@ export type SessionTagListOptions = { query?: string; } & OffsetPagination; -export type SessionTagList = { +type SessionTagList = { tags: string[]; }; -export type ListedSession = Prisma.SessionGetPayload<{ +type ListedSession = Prisma.SessionGetPayload<{ select: { id: true; friendlyId: true; @@ -193,10 +189,6 @@ export class SessionsRepository implements ISessionsRepository { } } -export function parseSessionListInputOptions(data: unknown): SessionListInputOptions { - return SessionListInputOptionsSchema.parse(data); -} - export function convertSessionListInputOptionsToFilterOptions( options: SessionListInputOptions ): FilterSessionsOptions { diff --git a/apps/webapp/app/services/signals.server.ts b/apps/webapp/app/services/signals.server.ts index b20df4ebdf5..0a64a3d333a 100644 --- a/apps/webapp/app/services/signals.server.ts +++ b/apps/webapp/app/services/signals.server.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "events"; import { singleton } from "~/utils/singleton"; -export type SignalsEvents = { +type SignalsEvents = { SIGTERM: [ { time: Date; @@ -16,10 +16,6 @@ export type SignalsEvents = { ]; }; -export type SignalsEventArgs = SignalsEvents[T]; - -export type SignalsEmitter = EventEmitter; - function initializeSignalsEmitter() { const emitter = new EventEmitter(); diff --git a/apps/webapp/app/services/slack.server.ts b/apps/webapp/app/services/slack.server.ts deleted file mode 100644 index 68010d68420..00000000000 --- a/apps/webapp/app/services/slack.server.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { WebClient } from "@slack/web-api"; -import { env } from "~/env.server"; -import { logger } from "./logger.server"; - -const slack = new WebClient(env.SLACK_BOT_TOKEN); - -type SendNewOrgMessageParams = { - orgName: string; - whyUseUs: string; - userEmail: string; -}; - -export async function sendNewOrgMessage({ orgName, whyUseUs, userEmail }: SendNewOrgMessageParams) { - if (!env.SLACK_BOT_TOKEN || !env.SLACK_SIGNUP_REASON_CHANNEL_ID) { - return; - } - try { - await slack.chat.postMessage({ - channel: env.SLACK_SIGNUP_REASON_CHANNEL_ID, - text: `New org created: ${orgName}`, - blocks: [ - { - type: "header", - text: { type: "plain_text", text: "New org created" }, - }, - { - type: "section", - text: { type: "mrkdwn", text: `*Org name:* ${orgName}` }, - }, - { - type: "section", - text: { type: "mrkdwn", text: `*What problem are you trying to solve?*\n${whyUseUs}` }, - }, - { - type: "context", - elements: [{ type: "mrkdwn", text: `Created by: ${userEmail}` }], - }, - ], - }); - } catch (error) { - logger.error("Error sending data to Slack when creating an org:", { error }); - } -} diff --git a/apps/webapp/app/services/ssoAuth.server.ts b/apps/webapp/app/services/ssoAuth.server.ts index 581a98bdfe2..2b0fbbf8c29 100644 --- a/apps/webapp/app/services/ssoAuth.server.ts +++ b/apps/webapp/app/services/ssoAuth.server.ts @@ -11,7 +11,7 @@ import { logger } from "./logger.server"; import { postAuthentication } from "./postAuth.server"; import { ssoController } from "./sso.server"; -export type SsoVerifyParams = { +type SsoVerifyParams = { profile: SsoProfile; flow: SsoFlow; }; diff --git a/apps/webapp/app/services/taskIdentifierCache.server.ts b/apps/webapp/app/services/taskIdentifierCache.server.ts index 04929c583cc..4c33dc37649 100644 --- a/apps/webapp/app/services/taskIdentifierCache.server.ts +++ b/apps/webapp/app/services/taskIdentifierCache.server.ts @@ -83,20 +83,6 @@ export async function populateTaskIdentifierCache( } } -export async function invalidateTaskIdentifierCache(environmentId: string): Promise { - if (!redis) return; - - try { - const key = buildKey(environmentId); - await redis.del(key); - } catch (error) { - logger.error("Failed to invalidate task identifier cache", { - environmentId, - error, - }); - } -} - export async function getTaskIdentifiersFromCache( environmentId: string ): Promise { diff --git a/apps/webapp/app/services/userActorEnvironment.server.ts b/apps/webapp/app/services/userActorEnvironment.server.ts index 69b3a5bec01..015c6b41ec6 100644 --- a/apps/webapp/app/services/userActorEnvironment.server.ts +++ b/apps/webapp/app/services/userActorEnvironment.server.ts @@ -13,7 +13,7 @@ import { json } from "@remix-run/server-runtime"; import { type UserActorClaims } from "@trigger.dev/rbac"; import { $replica } from "~/db.server"; -export const FORBIDDEN_ENVIRONMENT_CODE = "forbidden_environment"; +const FORBIDDEN_ENVIRONMENT_CODE = "forbidden_environment"; const DASHBOARD_AGENT_CLIENT = "dashboard-agent"; diff --git a/apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts b/apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts index 32a9b4923bc..c0faa53f7de 100644 --- a/apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts +++ b/apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts @@ -118,7 +118,7 @@ export interface IWebhookDeliveriesRepository { getDelivery(options: GetWebhookDeliveryOptions): Promise; } -export class WebhookDeliveriesRepository implements IWebhookDeliveriesRepository { +class WebhookDeliveriesRepository implements IWebhookDeliveriesRepository { private readonly clickHouseRepository: ClickHouseWebhookDeliveriesRepository; constructor(private readonly options: WebhookDeliveriesRepositoryOptions) { diff --git a/apps/webapp/app/utils.ts b/apps/webapp/app/utils.ts index 680c2a0c9f2..76fbcb4a260 100644 --- a/apps/webapp/app/utils.ts +++ b/apps/webapp/app/utils.ts @@ -1,6 +1,3 @@ -import type { UIMatch } from "@remix-run/react"; -import { useMatches } from "@remix-run/react"; - const DEFAULT_REDIRECT = "/"; // Pathnames that are NOT user-navigable destinations: fetcher endpoints, @@ -67,73 +64,6 @@ export function sanitizeRedirectPath( return path; } -/** - * This base hook is used in other hooks to quickly search for specific data - * across all loader data using useMatches. - * @param {string} id The route id - * @returns {JSON|undefined} The router data or undefined if not found - */ -export function useMatchesData(id: string | string[], debug: boolean = false): UIMatch | undefined { - const matchingRoutes = useMatches(); - - if (debug) { - console.log("matchingRoutes", matchingRoutes); - } - - const paths = Array.isArray(id) ? id : [id]; - - // Get the first matching route - const route = paths.reduce( - (acc, path) => { - if (acc) return acc; - return matchingRoutes.find((route) => route.id === path); - }, - undefined as UIMatch | undefined - ); - - return route; -} - -export function validateEmail(email: unknown): email is string { - return typeof email === "string" && email.length > 3 && email.includes("@"); -} - -export function hydrateObject(object: any): T { - return hydrateDates(object) as T; -} - -export function hydrateDates(object: any): any { - if (object === null || object === undefined) { - return object; - } - - if (object instanceof Date) { - return object; - } - - if ( - typeof object === "string" && - object.match(/\d{4}-\d{2}-\d{2}/) && - !Number.isNaN(Date.parse(object)) - ) { - return new Date(object); - } - - if (typeof object === "object") { - if (Array.isArray(object)) { - return object.map((item) => hydrateDates(item)); - } else { - const hydratedObject: any = {}; - for (const key in object) { - hydratedObject[key] = hydrateDates(object[key]); - } - return hydratedObject; - } - } - - return object; -} - export function titleCase(original: string): string { return original .split(" ") @@ -141,12 +71,6 @@ export function titleCase(original: string): string { .join(" "); } -// Takes an api key (either trigger_live_xxxx or trigger_development_xxxx) and returns trigger_live_******** -export const obfuscateApiKey = (apiKey: string) => { - const [prefix, slug, secretPart] = apiKey.split("_"); - return `${prefix}_${slug}_${"*".repeat(secretPart.length)}`; -}; - export function appEnvTitleTag(appEnv?: string): string { if (!appEnv || appEnv === "production") { return ""; diff --git a/apps/webapp/app/utils/apiCors.ts b/apps/webapp/app/utils/apiCors.ts index fc07fadcc2f..30235828bd6 100644 --- a/apps/webapp/app/utils/apiCors.ts +++ b/apps/webapp/app/utils/apiCors.ts @@ -23,13 +23,6 @@ export async function apiCors( return cors(request, response, options); } -export function makeApiCors( - request: Request, - options: CorsOptions = { maxAge: 5 * 60 } -): (response: Response) => Promise { - return (response: Response) => apiCors(request, response, options); -} - function hasCorsHeaders(response: Response) { return response.headers.has("access-control-allow-origin"); } diff --git a/apps/webapp/app/utils/cspImageOrigins.ts b/apps/webapp/app/utils/cspImageOrigins.ts index 267c72df929..5d52c4b77fe 100644 --- a/apps/webapp/app/utils/cspImageOrigins.ts +++ b/apps/webapp/app/utils/cspImageOrigins.ts @@ -27,7 +27,7 @@ export const BASE_IMG_SRC_SOURCES = [ "https://trigger.dev/changelog/", ] as const; -export type RejectedOrigin = { value: string; reason: string }; +type RejectedOrigin = { value: string; reason: string }; export type ParsedImageOrigins = { /** Accepted, canonicalised (`scheme://host[:port]`) and deduplicated. */ diff --git a/apps/webapp/app/utils/databaseMetrics.server.ts b/apps/webapp/app/utils/databaseMetrics.server.ts index 2d075f7b707..f663e4f60c5 100644 --- a/apps/webapp/app/utils/databaseMetrics.server.ts +++ b/apps/webapp/app/utils/databaseMetrics.server.ts @@ -30,7 +30,7 @@ export type DatabaseMetricsSource = { poolCounters?: { opened: () => number; closed: () => number }; }; -export type NormalizedPoolMetrics = { +type NormalizedPoolMetrics = { open: number; busy: number; idle: number; @@ -59,14 +59,6 @@ export function registerDatabaseMetricsSource(source: DatabaseMetricsSource): vo sources.set(source.clientType, source); } -export function listDatabaseMetricsSources(): ReadonlyArray { - return Array.from(sources.values()); -} - -export function resetDatabaseMetricsSources(): void { - sources.clear(); -} - function indexByKey(entries: Array<{ key: string; value: number }>): Record { const out: Record = {}; for (const entry of entries) { diff --git a/apps/webapp/app/utils/delays.ts b/apps/webapp/app/utils/delays.ts index 1c498d6d4f8..5a2d12a8030 100644 --- a/apps/webapp/app/utils/delays.ts +++ b/apps/webapp/app/utils/delays.ts @@ -1,19 +1,5 @@ import { parseNaturalLanguageDuration } from "@trigger.dev/core/v3/isomorphic"; -export const calculateDurationInMs = (options: { - seconds?: number; - minutes?: number; - hours?: number; - days?: number; -}) => { - return ( - (options?.seconds ?? 0) * 1000 + - (options?.minutes ?? 0) * 60 * 1000 + - (options?.hours ?? 0) * 60 * 60 * 1000 + - (options?.days ?? 0) * 24 * 60 * 60 * 1000 - ); -}; - export async function parseDelay(value?: string | Date): Promise { if (!value) { return; diff --git a/apps/webapp/app/utils/inviteRoleLadder.ts b/apps/webapp/app/utils/inviteRoleLadder.ts index e0bd9f7471f..5cf321f157d 100644 --- a/apps/webapp/app/utils/inviteRoleLadder.ts +++ b/apps/webapp/app/utils/inviteRoleLadder.ts @@ -5,7 +5,7 @@ export type LadderRole = { id: string }; -export function buildRoleLevel(roles: ReadonlyArray): Record { +function buildRoleLevel(roles: ReadonlyArray): Record { const level: Record = {}; roles.forEach((r, i) => { // Top of the array = highest level; larger number means more authority. diff --git a/apps/webapp/app/utils/json.ts b/apps/webapp/app/utils/json.ts index b3b44055958..e949eff213a 100644 --- a/apps/webapp/app/utils/json.ts +++ b/apps/webapp/app/utils/json.ts @@ -1,5 +1,3 @@ -import type { z } from "zod"; - export function safeJsonParse(json?: string): unknown { if (!json) { return; @@ -11,56 +9,3 @@ export function safeJsonParse(json?: string): unknown { return null; } } - -export function safeJsonZodParse( - schema: z.Schema, - json: string -): z.SafeParseReturnType | undefined { - const parsed = safeJsonParse(json); - - if (parsed === null) { - return; - } - - return schema.safeParse(parsed); -} - -export async function safeJsonFromResponse(response: Response) { - const json = await response.text(); - return safeJsonParse(json); -} - -export async function safeBodyFromResponse( - response: Response, - schema: z.Schema -): Promise { - const json = await response.text(); - const unknownJson = safeJsonParse(json); - - if (!unknownJson) { - return; - } - - const parsedJson = schema.safeParse(unknownJson); - - if (parsedJson.success) { - return parsedJson.data; - } -} - -export async function safeParseBodyFromResponse( - response: Response, - schema: z.Schema -): Promise | undefined> { - try { - const unknownJson = await response.json(); - - if (!unknownJson) { - return; - } - - const parsedJson = schema.safeParse(unknownJson); - - return parsedJson; - } catch (_error) {} -} diff --git a/apps/webapp/app/utils/lerp.ts b/apps/webapp/app/utils/lerp.ts index c2df9a5d2d2..ccffd862cdb 100644 --- a/apps/webapp/app/utils/lerp.ts +++ b/apps/webapp/app/utils/lerp.ts @@ -10,6 +10,6 @@ export function inverseLerp(min: number, max: number, value: number) { } /** Clamps a value between a min and max */ -export function clamp(value: number, min: number, max: number) { +function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); } diff --git a/apps/webapp/app/utils/logUtils.ts b/apps/webapp/app/utils/logUtils.ts index b5028387b4b..140106757b0 100644 --- a/apps/webapp/app/utils/logUtils.ts +++ b/apps/webapp/app/utils/logUtils.ts @@ -4,8 +4,6 @@ import { z } from "zod"; export const LogLevelSchema = z.enum(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]); export type LogLevel = z.infer; -export const validLogLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]; - // Default styles for search highlighting const DEFAULT_HIGHLIGHT_STYLES: React.CSSProperties = { backgroundColor: "#facc15", // yellow-400 diff --git a/apps/webapp/app/utils/modelFormatters.ts b/apps/webapp/app/utils/modelFormatters.ts index 9dffc395fb6..5d33963c873 100644 --- a/apps/webapp/app/utils/modelFormatters.ts +++ b/apps/webapp/app/utils/modelFormatters.ts @@ -32,9 +32,6 @@ export function formatFeature(slug: string): string { .join(" "); } -/** @deprecated Use formatFeature instead. */ -export const formatCapability = formatFeature; - /** Capitalize a provider name. */ export function formatProviderName(provider: string): string { const names: Record = { diff --git a/apps/webapp/app/utils/objects.ts b/apps/webapp/app/utils/objects.ts deleted file mode 100644 index 337fba9cac6..00000000000 --- a/apps/webapp/app/utils/objects.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function omit, K extends keyof T>( - obj: T, - keys: K[] -): Omit { - const result: any = {}; - - for (const key of Object.keys(obj)) { - if (!keys.includes(key as K)) { - result[key] = obj[key]; - } - } - - return result; -} diff --git a/apps/webapp/app/utils/pageSwitching.ts b/apps/webapp/app/utils/pageSwitching.ts index 6122e714002..bd9b4524679 100644 --- a/apps/webapp/app/utils/pageSwitching.ts +++ b/apps/webapp/app/utils/pageSwitching.ts @@ -1,7 +1,7 @@ import { type Path } from "@remix-run/react"; import { ENV_PAGE_TARGETS } from "./deeplinkPages"; -export const PORTABLE_PAGE_PARAM = "page"; +const PORTABLE_PAGE_PARAM = "page"; export const ENVIRONMENT_MATCH_ID = "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam"; diff --git a/apps/webapp/app/utils/pageTitle.ts b/apps/webapp/app/utils/pageTitle.ts index 3942cec7825..5c2cc8689f0 100644 --- a/apps/webapp/app/utils/pageTitle.ts +++ b/apps/webapp/app/utils/pageTitle.ts @@ -24,7 +24,7 @@ const APP_NAME = "Trigger.dev"; const ORGANIZATION_MATCH_ID = "routes/_app.orgs.$organizationSlug"; /** One or more title segments, most specific first: `["run_abc", "Runs"]`. */ -export type TitleSegments = string | string[]; +type TitleSegments = string | string[]; type MetaArgs = Parameters[0]; type Matches = MetaArgs["matches"]; @@ -57,7 +57,7 @@ export function pageMeta(page: PageInput): MetaFunct } /** Builds the full title from the page segments, the org scope and the app title. */ -export function composePageTitle(segments: string[], matches: Matches): string { +function composePageTitle(segments: string[], matches: Matches): string { return [...segments, scopeFromMatches(matches), appTitle(appEnvFromMatches(matches))] .filter((segment): segment is string => Boolean(segment)) .join(" | "); @@ -67,7 +67,7 @@ export function composePageTitle(segments: string[], matches: Matches): string { * The organization, and only on its own pages: inside a project the tab is already about one * project, and the dashboard switches projects in every tab at once, so naming it adds nothing. */ -export function scopeFromMatches(matches: Matches): string | undefined { +function scopeFromMatches(matches: Matches): string | undefined { const match = matches.find((m) => m.id === ORGANIZATION_MATCH_ID); if (!match || match.params?.projectParam) return undefined; const data = match.data as { organization?: { title?: string | null } } | undefined; diff --git a/apps/webapp/app/utils/parseRequestJson.server.ts b/apps/webapp/app/utils/parseRequestJson.server.ts deleted file mode 100644 index 461de5e2b78..00000000000 --- a/apps/webapp/app/utils/parseRequestJson.server.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Attributes } from "@opentelemetry/api"; -import { startActiveSpan } from "~/v3/tracer.server"; - -export async function parseRequestJsonAsync( - request: Request, - attributes?: Attributes -): Promise { - return await startActiveSpan( - "parseRequestJsonAsync()", - async (span) => { - span.setAttribute("content-length", parseInt(request.headers.get("content-length") ?? "0")); - span.setAttribute("content-type", request.headers.get("content-type") ?? "application/json"); - span.setAttribute("experiment.async", false); - - const rawText = await startActiveSpan("request.text()", async () => { - return await request.text(); - }); - - if (rawText.length === 0) { - return; - } - - return JSON.parse(rawText); - }, - { - attributes, - } - ); -} diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index 59cba150017..bd8cf152b78 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -170,7 +170,7 @@ export function organizationSettingsPath(organization: OrgForPath) { return `${organizationPath(organization)}/settings`; } -export function organizationIntegrationsPath(organization: OrgForPath) { +function organizationIntegrationsPath(organization: OrgForPath) { return `${organizationPath(organization)}/settings/integrations`; } @@ -216,10 +216,6 @@ export function vercelAppInstallPath(organizationSlug: string, projectSlug: stri return `/vercel/install?org_slug=${organizationSlug}&project_slug=${projectSlug}`; } -export function vercelCallbackPath() { - return `/vercel/callback`; -} - export function vercelResourcePath( organizationSlug: string, projectSlug: string, @@ -238,14 +234,6 @@ export function v3EnvironmentPath( )}/env/${environmentParam(environment)}`; } -export function v3TasksDashboardPath( - organization: OrgForPath, - project: ProjectForPath, - environment: EnvironmentForPath -) { - return `${v3EnvironmentPath(organization, project, environment)}/tasks/dashboard`; -} - export function v3TasksStreamingPath( organization: OrgForPath, project: ProjectForPath, @@ -374,7 +362,7 @@ export function v3TestTaskPath( )}`; } -export function v3PlaygroundPath( +function v3PlaygroundPath( organization: OrgForPath, project: ProjectForPath, environment: EnvironmentForPath @@ -678,7 +666,7 @@ export function v3BatchRunsPath( return `${v3RunsPath(organization, project, environment, { batchId: batch.friendlyId })}`; } -export function v3ProjectSettingsPath( +function v3ProjectSettingsPath( organization: OrgForPath, project: ProjectForPath, environment: EnvironmentForPath @@ -737,15 +725,6 @@ export function v3ModelsPath( return `${v3EnvironmentPath(organization, project, environment)}/models`; } -export function v3ModelDetailPath( - organization: OrgForPath, - project: ProjectForPath, - environment: EnvironmentForPath, - modelId: string -) { - return `${v3ModelsPath(organization, project, environment)}/${modelId}`; -} - export function v3ModelComparePath( organization: OrgForPath, project: ProjectForPath, @@ -857,19 +836,10 @@ export function v3BillingLimitsPath(organization: OrgForPath) { return `${organizationPath(organization)}/settings/billing-limits`; } -/** @deprecated Use v3BillingLimitsPath — redirects from billing-alerts are preserved */ -export function v3BillingAlertsPath(organization: OrgForPath) { - return v3BillingLimitsPath(organization); -} - export function v3PrivateConnectionsPath(organization: OrgForPath) { return `${organizationPath(organization)}/settings/private-connections`; } -export function v3NewPrivateConnectionPath(organization: OrgForPath) { - return `${organizationPath(organization)}/settings/private-connections/new`; -} - export function v3StripePortalPath(organization: OrgForPath) { return `/resources/${organization.slug}/subscription/portal`; } @@ -879,7 +849,7 @@ export function v3UsagePath(organization: OrgForPath) { } // Docs -export function docsRoot() { +function docsRoot() { return "https://trigger.dev/docs"; } @@ -887,10 +857,6 @@ export function docsPath(path: string) { return `${docsRoot()}/${path.replace(/^\//, "")}`; } -export function docsTroubleshootingPath(path: string) { - return `${docsRoot()}/v3/troubleshooting`; -} - export function adminPath() { return `/@`; } diff --git a/apps/webapp/app/utils/permissionDenied.ts b/apps/webapp/app/utils/permissionDenied.ts index b44b6941b1f..5ca08d96679 100644 --- a/apps/webapp/app/utils/permissionDenied.ts +++ b/apps/webapp/app/utils/permissionDenied.ts @@ -2,7 +2,7 @@ import { json } from "@remix-run/server-runtime"; // Marker on the thrown 403 body so the error boundary can tell a // permission denial apart from any other route error. -export const PERMISSION_DENIED_MARKER = "rbac-permission-denied"; +const PERMISSION_DENIED_MARKER = "rbac-permission-denied"; const DEFAULT_PERMISSION_DENIED_MESSAGE = "You don't have permission to access this page."; diff --git a/apps/webapp/app/utils/plainCustomerCards.ts b/apps/webapp/app/utils/plainCustomerCards.ts index 96303b0dec5..660719d7fc9 100644 --- a/apps/webapp/app/utils/plainCustomerCards.ts +++ b/apps/webapp/app/utils/plainCustomerCards.ts @@ -27,8 +27,6 @@ export const PlainCustomerCardRequestSchema = z.object({ .nullish(), }); -export type PlainCustomerCardRequest = z.infer; - /** * The values to try, in order, when looking a user up by email. * diff --git a/apps/webapp/app/utils/queryPerformanceMonitor.server.ts b/apps/webapp/app/utils/queryPerformanceMonitor.server.ts index 2398a49da10..8d2b746b2e0 100644 --- a/apps/webapp/app/utils/queryPerformanceMonitor.server.ts +++ b/apps/webapp/app/utils/queryPerformanceMonitor.server.ts @@ -1,12 +1,12 @@ import { env } from "~/env.server"; import { logger } from "~/services/logger.server"; -export interface QueryPerformanceConfig { +interface QueryPerformanceConfig { verySlowQueryThreshold?: number; // ms maxQueryLogLength: number; } -export class QueryPerformanceMonitor { +class QueryPerformanceMonitor { private config: QueryPerformanceConfig; constructor(config: Partial = {}) { diff --git a/apps/webapp/app/utils/redactor.ts b/apps/webapp/app/utils/redactor.ts deleted file mode 100644 index 4739590980f..00000000000 --- a/apps/webapp/app/utils/redactor.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Redacts the given object based on the given paths -// Example: -// const redactor = new Redactor(["data.object.balance_transaction"]); -// redactor.redact({ -// data: { -// object: { -// balance_transaction: "txn_1NYWgTI0XSgju2urW3aXpinM", -// }, -// }, -// }); -// Returns: -// { -// data: { -// object: { -// balance_transaction: "[REDACTED]", -// }, -// }, -// } -// Does not currenly support arrays -export class Redactor { - constructor(private paths: string[]) {} - - public redact(subject: unknown): unknown { - if (!Array.isArray(this.paths)) { - return subject; - } - - if (this.paths.length === 0) { - return subject; - } - - const clonedSubject = JSON.parse(JSON.stringify(subject)); - - return this.redactPathsRecursive(clonedSubject, this.paths); - } - - private redactPathsRecursive(subject: any, paths: string[]): any { - for (let path of paths) { - let parts = path.split("."); - - let curSubject = subject; - - // Make sure curSubject is an object - if (typeof curSubject !== "object") { - break; - } - - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - - if (Object.prototype.hasOwnProperty.call(curSubject, part) === false) { - // Path is not found in object - break; - } - - if (i === parts.length - 1) { - // We're at the end of our path and have a string, redact it - curSubject[part] = "[REDACTED]"; - } else if (part in curSubject && typeof curSubject[part] === "object") { - // More paths to follow, continue down the path - curSubject = curSubject[part]; - } else { - // Path is not found in object or doesn't point to a string - break; - } - } - } - - return subject; - } -} diff --git a/apps/webapp/app/utils/semver.ts b/apps/webapp/app/utils/semver.ts index e53abf9ee09..46f42f4b2aa 100644 --- a/apps/webapp/app/utils/semver.ts +++ b/apps/webapp/app/utils/semver.ts @@ -16,7 +16,7 @@ function parseVersionParts(version: string): number[] { * Falls back to lexicographic comparison when segments are equal. * Returns a negative number if `a` should come before `b` (i.e. `a` is newer). */ -export function compareVersionsDescending(a: string, b: string): number { +function compareVersionsDescending(a: string, b: string): number { const partsA = parseVersionParts(a); const partsB = parseVersionParts(b); const maxLen = Math.max(partsA.length, partsB.length); diff --git a/apps/webapp/app/utils/sse.ts b/apps/webapp/app/utils/sse.ts index 53f9aa010cd..4970b87cb94 100644 --- a/apps/webapp/app/utils/sse.ts +++ b/apps/webapp/app/utils/sse.ts @@ -45,12 +45,12 @@ const connections: Set = new Set(); // AbortSignal.any composite — see comment near the timeoutTimer below for the // Node issue refs), but naming the sentinels keeps call sites readable and // lets future signal.reason consumers branch on the cause. -export const ABORT_REASON_REQUEST = "request_aborted"; -export const ABORT_REASON_TIMEOUT = "timeout"; +const ABORT_REASON_REQUEST = "request_aborted"; +const ABORT_REASON_TIMEOUT = "timeout"; export const ABORT_REASON_SEND_ERROR = "send_error"; -export const ABORT_REASON_INIT_STOP = "init_requested_stop"; -export const ABORT_REASON_ITERATOR_STOP = "iterator_requested_stop"; -export const ABORT_REASON_ITERATOR_ERROR = "iterator_error"; +const ABORT_REASON_INIT_STOP = "init_requested_stop"; +const ABORT_REASON_ITERATOR_STOP = "iterator_requested_stop"; +const ABORT_REASON_ITERATOR_ERROR = "iterator_error"; export function createSSELoader(options: SSEOptions) { const { timeout, interval = 500, debug = false, handler } = options; diff --git a/apps/webapp/app/utils/tablerIcons.ts b/apps/webapp/app/utils/tablerIcons.ts index e188e779bf7..559f08356a8 100644 --- a/apps/webapp/app/utils/tablerIcons.ts +++ b/apps/webapp/app/utils/tablerIcons.ts @@ -4820,5 +4820,3 @@ const tablerIconNames = [ ]; export const tablerIcons = new Set(tablerIconNames); - -export const tablerIconsFilled = new Set(tablerIconNames.filter((i) => i.endsWith("-filled"))); diff --git a/apps/webapp/app/utils/taskListToTree.ts b/apps/webapp/app/utils/taskListToTree.ts deleted file mode 100644 index 6b28fd86b44..00000000000 --- a/apps/webapp/app/utils/taskListToTree.ts +++ /dev/null @@ -1,30 +0,0 @@ -type InputType = { id: string; parentId: string | null }; -export type OutputType = T & { subtasks?: T[] }; - -export function taskListToTree( - tasks: T[], - addSubtasks = true -): OutputType[] { - if (!addSubtasks) { - return tasks; - } - - const result: OutputType[] = []; - const map = new Map(tasks.map((v) => [v.id, v])); - - for (const node of tasks) { - const parent: OutputType | null = node.parentId - ? (map.get(node.parentId) as OutputType) - : null; - if (parent) { - if (!parent.subtasks) { - parent.subtasks = [] as any; - } - parent.subtasks!.push(node as any); - } else { - result.push(node as any); - } - } - - return result; -} diff --git a/apps/webapp/app/utils/themePreference.ts b/apps/webapp/app/utils/themePreference.ts index 2b6408c2abc..a23f6cba999 100644 --- a/apps/webapp/app/utils/themePreference.ts +++ b/apps/webapp/app/utils/themePreference.ts @@ -14,7 +14,7 @@ export function normalizeThemePreference(value: unknown): ThemePreference { } /** The default dark theme ships with a slight contrast bump. */ -export const DEFAULT_THEME_CONTRAST = 50; +const DEFAULT_THEME_CONTRAST = 50; /** Interface contrast for the System themes, 0 to 100. Missing or invalid * values fall back to the default bump. */ diff --git a/apps/webapp/app/utils/timelineSpanEvents.ts b/apps/webapp/app/utils/timelineSpanEvents.ts index 1b956da3769..0ccc165899f 100644 --- a/apps/webapp/app/utils/timelineSpanEvents.ts +++ b/apps/webapp/app/utils/timelineSpanEvents.ts @@ -1,11 +1,9 @@ import type { SpanEvent } from "@trigger.dev/core/v3"; import { millisecondsToNanoseconds } from "@trigger.dev/core/v3/utils/durations"; -export type TimelineEventState = "complete" | "error" | "inprogress" | "delayed"; +type TimelineLineVariant = "light" | "normal"; -export type TimelineLineVariant = "light" | "normal"; - -export type TimelineEventVariant = +type TimelineEventVariant = | "start-cap" | "dot-hollow" | "dot-solid" diff --git a/apps/webapp/app/utils/webhookIngressUrl.server.ts b/apps/webapp/app/utils/webhookIngressUrl.server.ts index 8789fd7a2b3..90dc27a7d8c 100644 --- a/apps/webapp/app/utils/webhookIngressUrl.server.ts +++ b/apps/webapp/app/utils/webhookIngressUrl.server.ts @@ -2,7 +2,7 @@ import { env } from "~/env.server"; // Public origin webhook providers POST to. A dedicated WEBHOOK_INGRESS_ORIGIN (e.g. // https://webhook.trigger.dev) takes precedence; otherwise it rides the API/app origin. -export function webhookIngressOrigin(): string { +function webhookIngressOrigin(): string { return env.WEBHOOK_INGRESS_ORIGIN ?? env.API_ORIGIN ?? env.APP_ORIGIN; } diff --git a/apps/webapp/app/v3/billingLimitWorker.server.ts b/apps/webapp/app/v3/billingLimitWorker.server.ts index 6a9048991fb..a694ac6b1fc 100644 --- a/apps/webapp/app/v3/billingLimitWorker.server.ts +++ b/apps/webapp/app/v3/billingLimitWorker.server.ts @@ -156,7 +156,7 @@ async function scheduleBillingLimitReconcileTick(worker: ReturnType { - const { userId, isAdmin, isImpersonating, organizationSlug } = options; - - // 1. If env var is set then globally enabled - if (env.AI_FEATURES_ENABLED === "1") { - return true; - } - - // 2. Admins always have access - if (isAdmin || isImpersonating) { - return true; - } - - // 3. Check if org/global feature flag is on - const org = await prisma.organization.findFirst({ - where: { - slug: organizationSlug, - members: { some: { userId } }, - }, - select: { - featureFlags: true, - }, - }); - - const flag = makeFlag(); - const flagResult = await flag({ - key: FEATURE_FLAG.hasAiAccess, - defaultValue: false, - overrides: (org?.featureFlags as Record) ?? {}, - }); - if (flagResult) { - return true; - } - - // 4. Not enabled anywhere - return false; -} diff --git a/apps/webapp/app/v3/electricShape.server.ts b/apps/webapp/app/v3/electricShape.server.ts index 65d52032afb..5e151db21af 100644 --- a/apps/webapp/app/v3/electricShape.server.ts +++ b/apps/webapp/app/v3/electricShape.server.ts @@ -10,7 +10,7 @@ export const UNSAFE_REALTIME_TAG_CHARS = /[\x00-\x1f\x7f\\"]/; * Sanitise a tag value for interpolation into an Electric Shape `where` clause: * reject unsafe chars, escape single quotes per SQL standard. */ -export function sanitizeRealtimeTagForSql(tag: string): string { +function sanitizeRealtimeTagForSql(tag: string): string { if (typeof tag !== "string" || tag.length === 0) { throw new Error("Invalid realtime tag: empty"); } diff --git a/apps/webapp/app/v3/engineDeprecation.server.ts b/apps/webapp/app/v3/engineDeprecation.server.ts index a7a9ea35f7b..78001e392c7 100644 --- a/apps/webapp/app/v3/engineDeprecation.server.ts +++ b/apps/webapp/app/v3/engineDeprecation.server.ts @@ -1,7 +1,7 @@ // User-facing deprecation messages returned when a retired v3 (engine V1) SDK/CLI // still triggers, reschedules, or opens the legacy dev websocket. -export const V3_MIGRATION_URL = "https://trigger.dev/docs/migrating-from-v3"; +const V3_MIGRATION_URL = "https://trigger.dev/docs/migrating-from-v3"; export const V3_TRIGGER_DEPRECATION_MESSAGE = `Trigger.dev v3 is no longer supported. Please upgrade your project to v4 to keep triggering tasks: ${V3_MIGRATION_URL}`; diff --git a/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts b/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts index 02fe998fcac..b38e069e637 100644 --- a/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts +++ b/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts @@ -926,19 +926,6 @@ export class EnvironmentVariablesRepository implements Repository { } } -export const RuntimeEnvironmentForEnvRepoPayload = { - select: { - id: true, - slug: true, - type: true, - projectId: true, - apiKey: true, - organizationId: true, - branchName: true, - builtInEnvironmentVariableOverrides: true, - }, -} as const; - // Derived from the slim AuthenticatedEnvironment so a full AE satisfies // this type — the legacy Prisma payload had `builtInEnvironmentVariableOverrides` // as Prisma's JsonValue, which is a subtype of `unknown` in the slim @@ -956,7 +943,7 @@ export type RuntimeEnvironmentForEnvRepo = Pick< | "builtInEnvironmentVariableOverrides" > & { organization?: { featureFlags: unknown } | null }; -export const environmentVariablesRepository = new EnvironmentVariablesRepository(); +const environmentVariablesRepository = new EnvironmentVariablesRepository(); export async function resolveVariablesForEnvironment( runtimeEnvironment: RuntimeEnvironmentForEnvRepo, diff --git a/apps/webapp/app/v3/environmentVariables/repository.ts b/apps/webapp/app/v3/environmentVariables/repository.ts index 63c5561bfaf..ba0d70b6e04 100644 --- a/apps/webapp/app/v3/environmentVariables/repository.ts +++ b/apps/webapp/app/v3/environmentVariables/repository.ts @@ -6,7 +6,7 @@ export const EnvironmentVariableKey = z .nonempty("Key is required") .regex(/^\w+$/, "Keys can only use alphanumeric characters and underscores"); -export const EnvironmentVariableUpdaterSchema = z.discriminatedUnion("type", [ +const EnvironmentVariableUpdaterSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("user"), userId: z.string(), diff --git a/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts b/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts index ef81942164c..6f73f258f29 100644 --- a/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts +++ b/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts @@ -2749,52 +2749,6 @@ export const convertDateToClickhouseDateTime = (date: Date): string => { return date.toISOString().replace("T", " ").replace("Z", ""); }; -/** - * Convert a ClickHouse DateTime64 to nanoseconds since epoch (UTC). - * Accepts: - * - "2025-09-23 12:32:46.130262875" - * - "2025-09-23T12:32:46.13" - * - "2025-09-23 12:32:46Z" - * - "2025-09-23 12:32:46.130262875+02:00" - */ -export function convertClickhouseDateTime64ToNanosecondsEpoch(date: string): bigint { - const s = date.trim(); - const m = CLICKHOUSE_DATETIME_REGEX.exec(s); - if (!m) { - throw new Error(`Invalid ClickHouse DateTime64 string: "${date}"`); - } - - const year = Number(m[1]); - const month = Number(m[2]); // 1-12 - const day = Number(m[3]); // 1-31 - const hour = Number(m[4]); - const minute = Number(m[5]); - const second = Number(m[6]); - const fraction = m[7] ?? ""; // up to 9 digits - const sign = m[8] as "+" | "-" | undefined; - const offH = m[9] ? Number(m[9]) : 0; - const offM = m[10] ? Number(m[10]) : 0; - - // Convert fractional seconds to exactly 9 digits (nanoseconds within the second). - const nsWithinSecond = Number(fraction.padEnd(9, "0")); // 0..999_999_999 - - // Split into millisecond part (for Date) and leftover nanoseconds. - const msPart = Math.trunc(nsWithinSecond / 1_000_000); // 0..999 - const leftoverNs = nsWithinSecond - msPart * 1_000_000; // 0..999_999 - - // Build milliseconds since epoch in UTC using Date.UTC (avoids local TZ/DST issues). - let msEpoch = Date.UTC(year, month - 1, day, hour, minute, second, msPart); - - // If an explicit offset was provided, adjust to true UTC. - if (sign) { - const offsetMinutesSigned = (sign === "+" ? 1 : -1) * (offH * 60 + offM); - msEpoch -= offsetMinutesSigned * 60_000; - } - - // Combine ms to ns with leftover. - return BigInt(msEpoch) * 1_000_000n + BigInt(leftoverNs); -} - /** * Convert a ClickHouse DateTime64 to a JS Date. * Accepts: diff --git a/apps/webapp/app/v3/eventRepository/eventRepository.types.ts b/apps/webapp/app/v3/eventRepository/eventRepository.types.ts index d65999a8c27..a77d05d7090 100644 --- a/apps/webapp/app/v3/eventRepository/eventRepository.types.ts +++ b/apps/webapp/app/v3/eventRepository/eventRepository.types.ts @@ -1,6 +1,5 @@ import type { Attributes, Tracer } from "@opentelemetry/api"; import type { - ExceptionEventProperties, SpanEvents, TaskEventEnvironment, TaskEventStyle, @@ -8,7 +7,6 @@ import type { } from "@trigger.dev/core/v3"; import type { Prisma, - TaskEvent, TaskEventKind, TaskEventLevel, TaskEventStatus, @@ -16,7 +14,6 @@ import type { } from "@trigger.dev/database"; import type { MetricsV1Input } from "@internal/clickhouse"; import type { DetailedTraceEvent, TaskEventStoreTable } from "../taskEventStore.server"; -export type { ExceptionEventProperties }; // ============================================================================ // Event Creation Types @@ -123,7 +120,7 @@ export type TraceAttributes = Partial< > >; -export type SetAttribute = (key: keyof T, value: T[keyof T]) => void; +type SetAttribute = (key: keyof T, value: T[keyof T]) => void; export type TraceEventOptions = { kind?: CreatableEventKind; @@ -146,13 +143,6 @@ export type EventBuilder = { failWithError: (error: TaskRunError) => void; }; -export type UpdateEventOptions = { - attributes: TraceAttributes; - endTime?: Date; - immediate?: boolean; - events?: SpanEvents; -}; - // ============================================================================ // Configuration Types // ============================================================================ @@ -171,14 +161,6 @@ export type EventRepoConfig = { loadSheddingEnabled?: boolean; }; -// ============================================================================ -// Query Types -// ============================================================================ - -export type QueryOptions = Prisma.TaskEventWhereInput; - -export type TaskEventRecord = TaskEvent; - export type QueriedEvent = Prisma.TaskEventGetPayload<{ select: { spanId: true; diff --git a/apps/webapp/app/v3/eventRepository/index.server.ts b/apps/webapp/app/v3/eventRepository/index.server.ts index f0687d2a7ce..c599f4e6b96 100644 --- a/apps/webapp/app/v3/eventRepository/index.server.ts +++ b/apps/webapp/app/v3/eventRepository/index.server.ts @@ -110,38 +110,6 @@ export async function getEventRepository( } } -export async function getV3EventRepository( - organizationId: string, - parentStore: string | undefined -): Promise<{ repository: IEventRepository; store: string }> { - if (typeof parentStore === "string") { - // Support legacy Postgres store for self-hosters and runs persisted with a - // non-ClickHouse store — fall back to the Prisma-based event repository. - if ( - parentStore !== EVENT_STORE_TYPES.CLICKHOUSE && - parentStore !== EVENT_STORE_TYPES.CLICKHOUSE_V2 - ) { - return { repository: eventRepository, store: parentStore }; - } - - const { repository: resolvedRepository } = - await clickhouseFactory.getEventRepositoryForOrganization(parentStore, organizationId); - return { repository: resolvedRepository, store: parentStore }; - } - - if (env.EVENT_REPOSITORY_DEFAULT_STORE === "clickhouse_v2") { - const { repository: resolvedRepository } = - await clickhouseFactory.getEventRepositoryForOrganization("clickhouse_v2", organizationId); - return { repository: resolvedRepository, store: "clickhouse_v2" }; - } else if (env.EVENT_REPOSITORY_DEFAULT_STORE === "clickhouse") { - const { repository: resolvedRepository } = - await clickhouseFactory.getEventRepositoryForOrganization("clickhouse", organizationId); - return { repository: resolvedRepository, store: "clickhouse" }; - } else { - return { repository: eventRepository, store: getTaskEventStore() }; - } -} - async function resolveTaskEventRepositoryFlag( featureFlags: Record | undefined ): Promise<"clickhouse" | "clickhouse_v2" | "postgres"> { diff --git a/apps/webapp/app/v3/eventRepository/sanitizeRowsOnParseError.server.ts b/apps/webapp/app/v3/eventRepository/sanitizeRowsOnParseError.server.ts index 32d80ef43d4..5f0c67d3b0b 100644 --- a/apps/webapp/app/v3/eventRepository/sanitizeRowsOnParseError.server.ts +++ b/apps/webapp/app/v3/eventRepository/sanitizeRowsOnParseError.server.ts @@ -183,13 +183,13 @@ export function sanitizeRows(rows: T[]): SanitizeResult { return result; } -export function errorMessage(err: unknown): string { +function errorMessage(err: unknown): string { return typeof err === "object" && err !== null && "message" in err ? String((err as { message?: unknown }).message ?? "") : String(err); } -export function rawErrorMessage(err: unknown): string { +function rawErrorMessage(err: unknown): string { if (typeof err === "object" && err !== null) { const raw = (err as { rawMessage?: unknown }).rawMessage; if (typeof raw === "string" && raw.length > 0) return raw; @@ -213,7 +213,7 @@ export type JsonParseRecoveryLogger = { * insert. Both causes land the same way, so they share the `capped` flag and its * counter; this distinguishes them in logs. */ -export type RecoveryBailReason = +type RecoveryBailReason = /** The per-batch strip budget (`maxPoisonStrips`) was spent. A poison flood. */ | "strip_budget_spent" /** ClickHouse gave no usable `at row N` hint, so there was no row to strip. */ @@ -261,7 +261,7 @@ export function landedNothing(outcome: JsonParseRecoveryOutcome, batchSize: numb * limit, a burst of un-ingestable runs in one flush would re-parse a large * batch many times on the shared ClickHouse server. */ -export const DEFAULT_MAX_POISON_STRIPS = 1; +const DEFAULT_MAX_POISON_STRIPS = 1; /** * ClickHouse insert recovery for `Cannot parse JSON object` rejections on the diff --git a/apps/webapp/app/v3/eventRepository/traceExport.server.ts b/apps/webapp/app/v3/eventRepository/traceExport.server.ts index c0a736c60b0..cea3dce65ba 100644 --- a/apps/webapp/app/v3/eventRepository/traceExport.server.ts +++ b/apps/webapp/app/v3/eventRepository/traceExport.server.ts @@ -28,7 +28,7 @@ export type TraceExportFormat = { footer?: (ctx: TraceExportContext) => string; }; -export type TraceExportFormatName = "log" | "jsonl" | "markdown"; +type TraceExportFormatName = "log" | "jsonl" | "markdown"; /** * Streams a trace export by piping events through a {@link TraceExportFormat}. diff --git a/apps/webapp/app/v3/featureFlags.server.ts b/apps/webapp/app/v3/featureFlags.server.ts index e0a6d608b71..b32a4578640 100644 --- a/apps/webapp/app/v3/featureFlags.server.ts +++ b/apps/webapp/app/v3/featureFlags.server.ts @@ -104,12 +104,12 @@ export function makeSetFlag(_prisma: PrismaClientOrTransaction = prisma) { }; } -export type AllFlagsOptions = { +type AllFlagsOptions = { defaultValues?: Partial; overrides?: Record; }; -export function makeFlags(_prisma: PrismaClientOrTransaction = prisma) { +function makeFlags(_prisma: PrismaClientOrTransaction = prisma) { return async function flags(options?: AllFlagsOptions): Promise> { const rows = await _prisma.featureFlag.findMany(); @@ -156,7 +156,6 @@ export function makeFlags(_prisma: PrismaClientOrTransaction = prisma) { export const flag = makeFlag(); export const flags = makeFlags(); -export const setFlag = makeSetFlag(); // Utility function to set multiple feature flags at once export function makeSetMultipleFlags(_prisma: PrismaClientOrTransaction = prisma) { diff --git a/apps/webapp/app/v3/featureFlags.ts b/apps/webapp/app/v3/featureFlags.ts index 6b01c5adfad..390892b121a 100644 --- a/apps/webapp/app/v3/featureFlags.ts +++ b/apps/webapp/app/v3/featureFlags.ts @@ -135,11 +135,6 @@ export function validateFeatureFlagValue( return FeatureFlagCatalog[key].safeParse(value); } -// Utility function to validate all feature flags at once -export function validateAllFeatureFlags(values: Record) { - return FeatureFlagCatalogSchema.safeParse(values); -} - // Utility function to validate partial feature flags (all keys optional) export function validatePartialFeatureFlags(values: Record) { return FeatureFlagCatalogSchema.partial().safeParse(values); @@ -201,7 +196,7 @@ export type FlagControlType = | { type: "number"; min?: number; max?: number } | { type: "string" }; -export function getFlagControlType(schema: z.ZodTypeAny): FlagControlType { +function getFlagControlType(schema: z.ZodTypeAny): FlagControlType { const typeName = schema._def.typeName; if (typeName === "ZodBoolean") { diff --git a/apps/webapp/app/v3/models/workerDeployment.server.ts b/apps/webapp/app/v3/models/workerDeployment.server.ts index 56f880f7b8c..ba0c24cb4f3 100644 --- a/apps/webapp/app/v3/models/workerDeployment.server.ts +++ b/apps/webapp/app/v3/models/workerDeployment.server.ts @@ -1,35 +1,14 @@ -import type { Prettify } from "@trigger.dev/core"; import type { BackgroundWorker, PrismaClientOrTransaction, RunEngineVersion, WorkerDeploymentType, } from "@trigger.dev/database"; -import { - CURRENT_DEPLOYMENT_LABEL, - CURRENT_UNMANAGED_DEPLOYMENT_LABEL, -} from "@trigger.dev/core/v3/isomorphic"; +import { CURRENT_DEPLOYMENT_LABEL } from "@trigger.dev/core/v3/isomorphic"; import type { Prisma } from "~/db.server"; import { prisma } from "~/db.server"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; -export type CurrentWorkerDeployment = Prettify< - NonNullable>> ->; - -export type BackgroundWorkerTaskSlim = Prisma.BackgroundWorkerTaskGetPayload<{ - select: { - id: true; - friendlyId: true; - slug: true; - filePath: true; - exportName: true; - triggerSource: true; - machineConfig: true; - maxDurationInSeconds: true; - }; -}>; - type WorkerDeploymentWithWorkerTasks = Prisma.WorkerDeploymentGetPayload<{ select: { id: true; @@ -186,16 +165,6 @@ export async function getCurrentWorkerDeploymentEngineVersion( return undefined; } -export async function findCurrentUnmanagedWorkerDeployment( - environmentId: string -): Promise { - return await findCurrentWorkerDeployment({ - environmentId, - label: CURRENT_UNMANAGED_DEPLOYMENT_LABEL, - type: "UNMANAGED", - }); -} - export async function findCurrentWorkerFromEnvironment( environment: Pick, prismaClient: PrismaClientOrTransaction = prisma, @@ -232,75 +201,3 @@ export async function findCurrentWorkerFromEnvironment( return deployment?.worker ?? null; } } - -export async function findCurrentUnmanagedWorkerFromEnvironment( - environment: Pick, - prismaClient: PrismaClientOrTransaction = prisma -): Promise | null> { - if (environment.type === "DEVELOPMENT") { - return null; - } - - return await findCurrentWorkerFromEnvironment( - environment, - prismaClient, - CURRENT_UNMANAGED_DEPLOYMENT_LABEL - ); -} - -export async function getWorkerDeploymentFromWorker( - workerId: string -): Promise { - const worker = await prisma.backgroundWorker.findFirst({ - where: { - id: workerId, - }, - include: { - deployment: true, - tasks: true, - }, - }); - - if (!worker?.deployment) { - return; - } - - const { deployment, ...workerWithoutDeployment } = worker; - - return { - ...deployment, - worker: workerWithoutDeployment, - }; -} - -export async function getWorkerDeploymentFromWorkerTask( - workerTaskId: string -): Promise { - const workerTask = await prisma.backgroundWorkerTask.findFirst({ - where: { - id: workerTaskId, - }, - include: { - worker: { - include: { - deployment: true, - tasks: true, - }, - }, - }, - }); - - if (!workerTask?.worker.deployment) { - return; - } - - const { deployment, ...workerWithoutDeployment } = workerTask.worker; - - return { - ...deployment, - worker: workerWithoutDeployment, - }; -} diff --git a/apps/webapp/app/v3/mollifier/idempotencyClaim.server.ts b/apps/webapp/app/v3/mollifier/idempotencyClaim.server.ts index 191ff62058b..249cc32d965 100644 --- a/apps/webapp/app/v3/mollifier/idempotencyClaim.server.ts +++ b/apps/webapp/app/v3/mollifier/idempotencyClaim.server.ts @@ -10,12 +10,12 @@ import { getMollifierBuffer } from "./mollifierBuffer.server"; // Tunables. The TTL on the claim key is bounded by typical trigger-pipeline // dwell; long enough that a slow PG insert doesn't expire mid-flight, // short enough that a crashed claimant unblocks waiters quickly. -export const DEFAULT_CLAIM_TTL_SECONDS = 30; +const DEFAULT_CLAIM_TTL_SECONDS = 30; // safetyNetMs caps how long a waiter blocks before returning timed_out. // Matches the mutateWithFallback safety net so SDK retry policies don't // have to special-case this path. -export const DEFAULT_CLAIM_WAIT_MS = 5_000; -export const DEFAULT_CLAIM_POLL_MS = 25; +const DEFAULT_CLAIM_WAIT_MS = 5_000; +const DEFAULT_CLAIM_POLL_MS = 25; export type ClaimOrAwaitOutcome = // We own the claim. `token` MUST be passed to publishClaim/releaseClaim diff --git a/apps/webapp/app/v3/mollifier/mollifierGate.server.ts b/apps/webapp/app/v3/mollifier/mollifierGate.server.ts index 08790123887..1292ac6bbb8 100644 --- a/apps/webapp/app/v3/mollifier/mollifierGate.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierGate.server.ts @@ -128,7 +128,7 @@ export function makeResolveMollifierFlag(): (inputs: GateInputs) => Promise env.TRIGGER_MOLLIFIER_ENABLED === "1", isShadowModeOn: () => env.TRIGGER_MOLLIFIER_SHADOW_MODE === "1", resolveOrgFlag: resolveMollifierFlag, diff --git a/apps/webapp/app/v3/mollifier/mollifierMollify.server.ts b/apps/webapp/app/v3/mollifier/mollifierMollify.server.ts index 6ebcf4a2487..28391c487b5 100644 --- a/apps/webapp/app/v3/mollifier/mollifierMollify.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierMollify.server.ts @@ -3,7 +3,7 @@ import type { MollifierBuffer } from "@trigger.dev/redis-worker"; import { serialiseMollifierSnapshot, type MollifierSnapshot } from "./mollifierSnapshot.server"; import type { TripDecision } from "./mollifierGate.server"; -export type MollifyNotice = { +type MollifyNotice = { code: "mollifier.queued"; message: string; docs: string; diff --git a/apps/webapp/app/v3/mollifier/mollifierTelemetry.server.ts b/apps/webapp/app/v3/mollifier/mollifierTelemetry.server.ts index 6310ad9d51f..ba79c1a5dc3 100644 --- a/apps/webapp/app/v3/mollifier/mollifierTelemetry.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierTelemetry.server.ts @@ -2,7 +2,7 @@ import { getMeter } from "@internal/tracing"; const meter = getMeter("mollifier"); -export const mollifierDecisionsCounter = meter.createCounter("mollifier.decisions", { +const mollifierDecisionsCounter = meter.createCounter("mollifier.decisions", { description: "Count of mollifier gate decisions by outcome", }); @@ -49,22 +49,6 @@ export function recordDecision(outcome: DecisionOutcome, opts: RecordDecisionOpt // the Electric stream anyway so the eventual drainer-INSERT propagates // to the client; this counter is the signal of how often customers // subscribe inside the buffered window. -export const realtimeBufferedSubscriptionsCounter = meter.createCounter( - "mollifier.realtime_subscriptions.buffered", - { - description: - "Realtime subscriptions opened against a runId that exists only in the mollifier buffer", - } -); - -// No `envId` attribute — `envId` is a banned high-cardinality metric -// label per the repo's OTel rules. The structured warn log emitted -// alongside the counter tick (in `mollifierStaleSweep.server.ts`) -// carries the envId / orgId / runId for forensic drill-down; the -// metric stays an aggregate. -export function recordRealtimeBufferedSubscription(): void { - realtimeBufferedSubscriptionsCounter.add(1); -} // Counts buffer entries that have been waiting in the queue ZSET longer // than the configured stale threshold. Useful for historical "stale @@ -72,7 +56,7 @@ export function recordRealtimeBufferedSubscription(): void { // single stuck entry observed by N sweep ticks adds N to the counter, // so `rate()` over an alerting window reflects (entries × ticks), not // "entries that are stale right now". -export const staleEntriesCounter = meter.createCounter("mollifier.stale_entries", { +const staleEntriesCounter = meter.createCounter("mollifier.stale_entries", { description: "Mollifier buffer entries whose dwell exceeds the stale threshold (per sweep pass)", }); @@ -86,7 +70,7 @@ export function recordStaleEntry(): void { // the gauge drops back to 0 when the drainer catches up instead of // staying latched. Recommended alert: // mollifier_stale_entries_current > 0 for 5m -export const staleEntriesGauge = meter.createObservableGauge("mollifier.stale_entries.current", { +const staleEntriesGauge = meter.createObservableGauge("mollifier.stale_entries.current", { description: "Buffer entries whose dwell exceeds the stale threshold, as observed by the latest sweep pass", }); @@ -123,7 +107,7 @@ meter.addBatchObservableCallback( // // No `envId` attribute — same high-cardinality constraint as the other // mollifier gauges. The per-entry hash carries env/org for drill-down. -export const drainingCountGauge = meter.createObservableGauge("mollifier.draining.current", { +const drainingCountGauge = meter.createObservableGauge("mollifier.draining.current", { description: "Mollifier buffer entries currently in DRAINING state (popped but not yet acked/failed/requeued)", }); @@ -140,13 +124,3 @@ meter.addBatchObservableCallback( }, [drainingCountGauge] ); - -// Electric SQL's shape-stream protocol adds a `handle=` query param on -// every reconnect after the initial GET. Gating the realtime-buffered -// log/counter on its absence keeps the signal at one tick per -// subscription instead of one tick per ~20s live-poll iteration — -// without it the counter would over-count by the long-poll factor. -export function isInitialBufferedSubscriptionRequest(url: string | URL): boolean { - const u = typeof url === "string" ? new URL(url) : url; - return !u.searchParams.has("handle"); -} diff --git a/apps/webapp/app/v3/mollifier/mollifierTripEvaluator.server.ts b/apps/webapp/app/v3/mollifier/mollifierTripEvaluator.server.ts index 9032467d200..a53bfdad744 100644 --- a/apps/webapp/app/v3/mollifier/mollifierTripEvaluator.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierTripEvaluator.server.ts @@ -2,7 +2,7 @@ import type { MollifierBuffer } from "@trigger.dev/redis-worker"; import { logger } from "~/services/logger.server"; import type { GateInputs, TripDecision, TripEvaluator } from "./mollifierGate.server"; -export type TripEvaluatorOptions = { +type TripEvaluatorOptions = { windowMs: number; threshold: number; holdMs: number; diff --git a/apps/webapp/app/v3/mollifier/mutateWithFallback.server.ts b/apps/webapp/app/v3/mollifier/mutateWithFallback.server.ts index 8460fbe541a..fd256f5811b 100644 --- a/apps/webapp/app/v3/mollifier/mutateWithFallback.server.ts +++ b/apps/webapp/app/v3/mollifier/mutateWithFallback.server.ts @@ -12,13 +12,13 @@ import { logger } from "~/services/logger.server"; import { getMollifierBuffer } from "./mollifierBuffer.server"; // Wait/retry knobs. Exported for tests. -export const DEFAULT_SAFETY_NET_MS = 2_000; +const DEFAULT_SAFETY_NET_MS = 2_000; // Initial gap between buffer polls; grows by BACKOFF_FACTOR up to // DEFAULT_MAX_POLL_STEP_MS so a slow drain doesn't poll at a tight fixed // cadence for the whole safety-net budget. -export const DEFAULT_POLL_STEP_MS = 20; -export const DEFAULT_MAX_POLL_STEP_MS = 250; -export const DEFAULT_BACKOFF_FACTOR = 1.7; +const DEFAULT_POLL_STEP_MS = 20; +const DEFAULT_MAX_POLL_STEP_MS = 250; +const DEFAULT_BACKOFF_FACTOR = 1.7; export type MutateWithFallbackInput = { runId: string; diff --git a/apps/webapp/app/v3/querySchemas.ts b/apps/webapp/app/v3/querySchemas.ts index d7637f52d63..690bbaf5396 100644 --- a/apps/webapp/app/v3/querySchemas.ts +++ b/apps/webapp/app/v3/querySchemas.ts @@ -451,7 +451,7 @@ export const runsSchema: TableSchema = { /** * Schema definition for the metrics table (trigger_dev.metrics_v1) */ -export const metricsSchema: TableSchema = { +const metricsSchema: TableSchema = { name: "metrics", clickhouseName: "trigger_dev.metrics_v1", description: "Host and runtime metrics collected during task execution", @@ -614,7 +614,7 @@ export const metricsSchema: TableSchema = { * Pre-aggregated into 10-second buckets. Counter columns re-aggregate with sum(), * gauges with max(), and wait_quantiles with quantilesMerge() — never FINAL. */ -export const queueMetricsSchema: TableSchema = { +const queueMetricsSchema: TableSchema = { name: "queue_metrics", clickhouseName: "trigger_dev.queue_metrics_v1", description: "Per-queue depth, concurrency, throttling, and scheduling-delay metrics", @@ -942,7 +942,7 @@ export const envMetricsSchema: TableSchema = { /** * Schema definition for the llm_metrics table (trigger_dev.llm_metrics_v1) */ -export const llmMetricsSchema: TableSchema = { +const llmMetricsSchema: TableSchema = { name: "llm_metrics", clickhouseName: "trigger_dev.llm_metrics_v1", description: "LLM metrics: token usage, cost, performance, and behavior from GenAI spans", @@ -1203,7 +1203,7 @@ export const llmMetricsSchema: TableSchema = { * Schema definition for the llm_models table (trigger_dev.llm_model_aggregates_v1) * Global table — no tenant columns. Contains anonymized cross-tenant model performance data. */ -export const llmModelsSchema: TableSchema = { +const llmModelsSchema: TableSchema = { name: "llm_models", clickhouseName: "trigger_dev.llm_model_aggregates_v1", description: @@ -1303,7 +1303,7 @@ export const llmModelsSchema: TableSchema = { * (e.g. per-tenant fairness). Rows are activity-bound: a (queue, key, bucket) row exists * only when that key had events, so key cardinality cannot inflate the table. */ -export const queueMetricsByKeySchema: TableSchema = { +const queueMetricsByKeySchema: TableSchema = { name: "queue_metrics_by_key", clickhouseName: "trigger_dev.queue_metrics_ck_v1", description: "Per-concurrency-key queue metrics: backlog, throughput, and wait by key", diff --git a/apps/webapp/app/v3/queueDepthSeries.ts b/apps/webapp/app/v3/queueDepthSeries.ts index 925e3129b34..9ad05b25abb 100644 --- a/apps/webapp/app/v3/queueDepthSeries.ts +++ b/apps/webapp/app/v3/queueDepthSeries.ts @@ -9,7 +9,7 @@ export type QueueDepthBucketRow = { bucket: string; depth: number; throttled: nu export type QueueDepthGrid = { startMs: number; bucketIntervalMs: number; numBuckets: number }; /** Rows placed on the grid by bucket index. Rows outside the window are dropped. */ -export function indexQueueDepthRows( +function indexQueueDepthRows( rows: QueueDepthBucketRow[], grid: QueueDepthGrid ): Map { @@ -25,7 +25,7 @@ export function indexQueueDepthRows( } /** A fixed-width series per grid bucket, so a gap can never shift later points in time. */ -export function fillQueueDepthSeries( +function fillQueueDepthSeries( byIndex: Map, numBuckets: number ): { depth: number[]; throttled: number[] } { diff --git a/apps/webapp/app/v3/runOpsMigration/controlPlaneCache.server.ts b/apps/webapp/app/v3/runOpsMigration/controlPlaneCache.server.ts index fdfd6d92cef..f3005b34e75 100644 --- a/apps/webapp/app/v3/runOpsMigration/controlPlaneCache.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/controlPlaneCache.server.ts @@ -49,7 +49,7 @@ export type ResolvedEnv = { * ~62KB/query (and each cached entry stays small); `machineConfig`/`retryConfig` are read * at dequeue and stay. */ -export type ResolvedWorkerTask = { +type ResolvedWorkerTask = { id: string; slug: string; machineConfig: Prisma.JsonValue | null; @@ -67,7 +67,7 @@ export const resolvedWorkerTaskSelect = { } satisfies Prisma.BackgroundWorkerTaskSelect; /** Mirrors run-engine's `ResolvedTaskQueue` exactly. `id` + `name` (the matcher keys on both). */ -export type ResolvedTaskQueue = { +type ResolvedTaskQueue = { id: string; name: string; }; @@ -82,7 +82,7 @@ export const resolvedTaskQueueSelect = { * Mirrors run-engine's `ResolvedWorkerDeployment` exactly. Drops the unread heavy JSON columns * (`externalBuildData`, `buildServerMetadata`, `errorData`, `git`) from this single-row read. */ -export type ResolvedWorkerDeployment = { +type ResolvedWorkerDeployment = { id: string; friendlyId: string; imageReference: string | null; diff --git a/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts b/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts index d173f3958bd..f15230ec442 100644 --- a/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/readThrough.server.ts @@ -20,14 +20,14 @@ import { logger as defaultLogger } from "~/services/logger.server"; import { ownerEngine, UnclassifiableRunId } from "@trigger.dev/core/v3/isomorphic"; import { isSplitEnabled } from "./splitMode.server"; -export type ReadThroughSource = "new" | "legacy-replica"; +type ReadThroughSource = "new" | "legacy-replica"; export type ReadThroughResult = | { source: ReadThroughSource; value: T } | { source: "not-found" } | { source: "past-retention" }; -export type ReadThroughDeps = { +type ReadThroughDeps = { newClient?: PrismaReplicaClient; legacyReplica?: PrismaReplicaClient; /** Resolved boot constant; never `await`ed per-request when supplied. */ diff --git a/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts b/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts index 955bd90b94a..688f95bac03 100644 --- a/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/splitMode.server.ts @@ -79,6 +79,6 @@ export function isSplitEnabled(): Promise { return cached; } -export function __resetSplitModeCacheForTests(): void { +function __resetSplitModeCacheForTests(): void { cached = undefined; } diff --git a/apps/webapp/app/v3/scheduleEngine.server.ts b/apps/webapp/app/v3/scheduleEngine.server.ts index 9939c0c26ca..d16309af42e 100644 --- a/apps/webapp/app/v3/scheduleEngine.server.ts +++ b/apps/webapp/app/v3/scheduleEngine.server.ts @@ -12,8 +12,6 @@ import { ServiceValidationError } from "./services/common.server"; export const scheduleEngine = singleton("ScheduleEngine", createScheduleEngine); -export type { ScheduleEngine }; - async function isDevEnvironmentConnectedHandler(environmentId: string) { const environment = await prisma.runtimeEnvironment.findFirst({ where: { diff --git a/apps/webapp/app/v3/services/aiQueryService.server.ts b/apps/webapp/app/v3/services/aiQueryService.server.ts index 29007e5c157..d9ee1153966 100644 --- a/apps/webapp/app/v3/services/aiQueryService.server.ts +++ b/apps/webapp/app/v3/services/aiQueryService.server.ts @@ -12,17 +12,6 @@ import type { AITimeFilter } from "~/routes/_app.orgs.$organizationSlug.projects // Re-export for backwards compatibility export type { AITimeFilter }; -/** - * Stream event types for AI query generation - */ -export type AIQueryStreamEvent = - | { type: "thinking"; content: string } - | { type: "tool_call"; tool: string; args: unknown } - | { type: "tool_result"; tool: string; result: unknown } - | { type: "time_filter"; filter: AITimeFilter } - | { type: "result"; success: true; query: string; timeFilter?: AITimeFilter } - | { type: "result"; success: false; error: string }; - /** * Result type for non-streaming call */ diff --git a/apps/webapp/app/v3/services/aiTitleRateLimiter.server.ts b/apps/webapp/app/v3/services/aiTitleRateLimiter.server.ts index e2e8723453d..5f3814a21e7 100644 --- a/apps/webapp/app/v3/services/aiTitleRateLimiter.server.ts +++ b/apps/webapp/app/v3/services/aiTitleRateLimiter.server.ts @@ -7,7 +7,7 @@ import { singleton } from "~/utils/singleton"; // apiRateLimiter (only `/api/*`) does not cover, so it needs its own per-user // cap. Exported so the policy is asserted in tests rather than re-encoded. export const AI_TITLE_RATE_LIMIT_ATTEMPTS = 30; -export const AI_TITLE_RATE_LIMIT_WINDOW = "10 m" as const; +const AI_TITLE_RATE_LIMIT_WINDOW = "10 m" as const; /** * Build the ai-title per-user rate limiter. Production uses the env-derived diff --git a/apps/webapp/app/v3/services/alerts/errorGroupWebhook.server.ts b/apps/webapp/app/v3/services/alerts/errorGroupWebhook.server.ts index 1c0f939862c..46f54d9038c 100644 --- a/apps/webapp/app/v3/services/alerts/errorGroupWebhook.server.ts +++ b/apps/webapp/app/v3/services/alerts/errorGroupWebhook.server.ts @@ -1,7 +1,7 @@ import { nanoid } from "nanoid"; import type { ErrorWebhook } from "@trigger.dev/core/v3/schemas"; -export type ErrorAlertClassification = "new_issue" | "regression" | "unignored"; +type ErrorAlertClassification = "new_issue" | "regression" | "unignored"; export type ErrorGroupAlertData = { classification: ErrorAlertClassification; diff --git a/apps/webapp/app/v3/services/alerts/safeWebhookFetch.server.ts b/apps/webapp/app/v3/services/alerts/safeWebhookFetch.server.ts index cb3cba3ca3c..a155046e683 100644 --- a/apps/webapp/app/v3/services/alerts/safeWebhookFetch.server.ts +++ b/apps/webapp/app/v3/services/alerts/safeWebhookFetch.server.ts @@ -22,7 +22,7 @@ import { */ // Re-exported so callers/tests don't reach into the underlying module. -export { assertSafeWebhookUrl, assertSafeWebhookUrlLexical, UnsafeWebhookUrlError }; +export { assertSafeWebhookUrl, UnsafeWebhookUrlError }; const MAX_REDIRECTS = 5; diff --git a/apps/webapp/app/v3/services/billingLimit/billingLimitConstants.ts b/apps/webapp/app/v3/services/billingLimit/billingLimitConstants.ts index 5aea0f1afdd..0cb890d703b 100644 --- a/apps/webapp/app/v3/services/billingLimit/billingLimitConstants.ts +++ b/apps/webapp/app/v3/services/billingLimit/billingLimitConstants.ts @@ -6,8 +6,6 @@ export const BILLABLE_ENVIRONMENT_TYPES = [ "PREVIEW", ] as const satisfies RuntimeEnvironmentType[]; -export type BillableEnvironmentType = (typeof BILLABLE_ENVIRONMENT_TYPES)[number]; - export const BILLING_LIMIT_CONVERGE_BATCH_SIZE = 50; /** Max concurrent per-org billing limit lookups during reconciliation. */ diff --git a/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.ts b/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.ts index 05e2965287e..c7a0dc735e8 100644 --- a/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.ts +++ b/apps/webapp/app/v3/services/bulk/BulkActionV2.batchReadThrough.server.ts @@ -22,7 +22,7 @@ import { } from "~/db.server"; import { ownerEngine, UnclassifiableRunId } from "@trigger.dev/core/v3/isomorphic"; -export type SeamReadDeps = { +type SeamReadDeps = { /** * Resolved boot constant. REQUIRED here — the caller resolves it once per * request via `isSplitEnabled()`; this adapter never awaits it itself. diff --git a/apps/webapp/app/v3/services/bulk/BulkActionV2.server.ts b/apps/webapp/app/v3/services/bulk/BulkActionV2.server.ts index 362975a60b8..d3cd77b143c 100644 --- a/apps/webapp/app/v3/services/bulk/BulkActionV2.server.ts +++ b/apps/webapp/app/v3/services/bulk/BulkActionV2.server.ts @@ -534,7 +534,7 @@ export class BulkActionService extends BaseService { } } -export function freezeRunListFilters(filters: RunListInputFilters): RunListInputFilters { +function freezeRunListFilters(filters: RunListInputFilters): RunListInputFilters { const { cursor: _cursor, direction: _direction, diff --git a/apps/webapp/app/v3/services/concurrencySystem.server.ts b/apps/webapp/app/v3/services/concurrencySystem.server.ts index f030cb72e5f..51c51674234 100644 --- a/apps/webapp/app/v3/services/concurrencySystem.server.ts +++ b/apps/webapp/app/v3/services/concurrencySystem.server.ts @@ -11,14 +11,14 @@ export type ConcurrencySystemOptions = { reader: PrismaClientOrTransaction; }; -export type QueueInput = string | { type: "task" | "custom"; name: string }; +type QueueInput = string | { type: "task" | "custom"; name: string }; /** * The concurrency-limit override to apply to a queue. Either an absolute `limit` or a `percent` * of the environment's maximum concurrency limit. A bare `number` is accepted for backwards * compatibility and is treated as an absolute limit. */ -export type ConcurrencyLimitOverride = number | { limit: number } | { percent: number }; +type ConcurrencyLimitOverride = number | { limit: number } | { percent: number }; /** * Materializes an absolute concurrency limit from a percentage of the environment limit. diff --git a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts index 1dd6b1b34ce..f8a69e83d2b 100644 --- a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts @@ -46,7 +46,6 @@ import { projectPubSub } from "./projectPubSub.server"; import { assertNoDuplicateTaskIds } from "./duplicateTaskIds.server"; import { stripBackgroundWorkerMetadataForStorage } from "./stripBackgroundWorkerMetadataForStorage.server"; -export { stripBackgroundWorkerMetadataForStorage }; export class CreateBackgroundWorkerService extends BaseService { private readonly _taskMetaCache: TaskMetadataCache; diff --git a/apps/webapp/app/v3/services/duplicateTaskIds.server.ts b/apps/webapp/app/v3/services/duplicateTaskIds.server.ts index 4ade92fd9fd..3f3c218b140 100644 --- a/apps/webapp/app/v3/services/duplicateTaskIds.server.ts +++ b/apps/webapp/app/v3/services/duplicateTaskIds.server.ts @@ -11,7 +11,7 @@ type TaskIdResource = { * (regular tasks, scheduled tasks, agents, etc.) share a single id namespace, * so a schedule and a regular task that use the same id count as a duplicate. */ -export function findDuplicateTaskIds(tasks: Array): string[] { +function findDuplicateTaskIds(tasks: Array): string[] { const seen = new Set(); const duplicates = new Set(); diff --git a/apps/webapp/app/v3/services/projectPubSub.server.ts b/apps/webapp/app/v3/services/projectPubSub.server.ts index 0d9004fee14..8008cd0ce7c 100644 --- a/apps/webapp/app/v3/services/projectPubSub.server.ts +++ b/apps/webapp/app/v3/services/projectPubSub.server.ts @@ -1,6 +1,5 @@ import { z } from "zod"; import { singleton } from "~/utils/singleton"; -import type { ZodSubscriber } from "../utils/zodPubSub.server"; import { ZodPubSub } from "../utils/zodPubSub.server"; import { env } from "~/env.server"; import { Gauge } from "prom-client"; @@ -19,8 +18,6 @@ const messageCatalog = { }), }; -export type ProjectSubscriber = ZodSubscriber; - export const projectPubSub = singleton("projectPubSub", initializeProjectPubSub); function initializeProjectPubSub() { diff --git a/apps/webapp/app/v3/services/tracePubSub.server.ts b/apps/webapp/app/v3/services/tracePubSub.server.ts index 21871918e05..1bf277e91ae 100644 --- a/apps/webapp/app/v3/services/tracePubSub.server.ts +++ b/apps/webapp/app/v3/services/tracePubSub.server.ts @@ -6,11 +6,11 @@ import { singleton } from "~/utils/singleton"; import { Gauge } from "prom-client"; import { metricsRegister } from "~/metrics.server"; -export type TracePubSubOptions = { +type TracePubSubOptions = { redis: RedisWithClusterOptions; }; -export class TracePubSub { +class TracePubSub { private _publisher: RedisClient; private _subscriberCount = 0; diff --git a/apps/webapp/app/v3/services/worker/sanitizeWorkerHeaders.ts b/apps/webapp/app/v3/services/worker/sanitizeWorkerHeaders.ts index 47be4a728fb..bd1b1774702 100644 --- a/apps/webapp/app/v3/services/worker/sanitizeWorkerHeaders.ts +++ b/apps/webapp/app/v3/services/worker/sanitizeWorkerHeaders.ts @@ -2,7 +2,7 @@ import { WORKER_HEADERS } from "@trigger.dev/core/v3/workers"; // Secret-bearing headers to drop before logging request headers. // Dependency-free so the redaction is unit-tested directly. -export const SENSITIVE_WORKER_HEADERS = new Set([ +const SENSITIVE_WORKER_HEADERS = new Set([ "authorization", "cookie", WORKER_HEADERS.MANAGED_SECRET.toLowerCase(), diff --git a/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts b/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts index e8ff63d84ac..e66d5429c18 100644 --- a/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts +++ b/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts @@ -363,8 +363,8 @@ export class WorkerGroupTokenService extends WithRunEngine { } } -export const WorkerInstanceEnv = z.enum(["dev", "staging", "prod"]).default("prod"); -export type WorkerInstanceEnv = z.infer; +const WorkerInstanceEnv = z.enum(["dev", "staging", "prod"]).default("prod"); +type WorkerInstanceEnv = z.infer; export type AuthenticatedWorkerInstanceOptions = WithRunEngineOptions<{ type: WorkerInstanceGroupType; diff --git a/apps/webapp/app/v3/services/worker/workloadTokenAuthorization.server.ts b/apps/webapp/app/v3/services/worker/workloadTokenAuthorization.server.ts index a38097b62b2..f26a359d0a7 100644 --- a/apps/webapp/app/v3/services/worker/workloadTokenAuthorization.server.ts +++ b/apps/webapp/app/v3/services/worker/workloadTokenAuthorization.server.ts @@ -9,7 +9,7 @@ * Pure and env-import-free so it stays trivially testable. */ -export type CreatedAtGateOutcome = "grandfathered" | "suppressed"; +type CreatedAtGateOutcome = "grandfathered" | "suppressed"; export type CreatedAtGateEvaluation = { outcome: CreatedAtGateOutcome; diff --git a/apps/webapp/app/v3/taskEventStore.server.ts b/apps/webapp/app/v3/taskEventStore.server.ts index a92db8d4284..564081fb358 100644 --- a/apps/webapp/app/v3/taskEventStore.server.ts +++ b/apps/webapp/app/v3/taskEventStore.server.ts @@ -4,9 +4,7 @@ import { Prisma } from "@trigger.dev/database"; import type { PrismaClient, PrismaReplicaClient } from "~/db.server"; import { env } from "~/env.server"; import { clampToEmergencySpanCap } from "~/v3/eventRepository/emergencySpanCap.server"; - -export type CommonTaskEvent = Omit; -export type TraceEvent = Pick< +type TraceEvent = Pick< TaskEvent, | "spanId" | "parentId" diff --git a/apps/webapp/app/v3/taskStatus.ts b/apps/webapp/app/v3/taskStatus.ts index 8606bcdafce..a2d9b8fc64b 100644 --- a/apps/webapp/app/v3/taskStatus.ts +++ b/apps/webapp/app/v3/taskStatus.ts @@ -13,7 +13,7 @@ export const FINAL_RUN_STATUSES = [ export type FINAL_RUN_STATUSES = (typeof FINAL_RUN_STATUSES)[number]; -export const NON_FINAL_RUN_STATUSES = [ +const NON_FINAL_RUN_STATUSES = [ "DELAYED", "PENDING", "PENDING_VERSION", @@ -25,15 +25,15 @@ export const NON_FINAL_RUN_STATUSES = [ "PAUSED", ] satisfies TaskRunStatus[]; -export type NON_FINAL_RUN_STATUSES = (typeof NON_FINAL_RUN_STATUSES)[number]; +type NON_FINAL_RUN_STATUSES = (typeof NON_FINAL_RUN_STATUSES)[number]; -export const PENDING_STATUSES = [ +const PENDING_STATUSES = [ "PENDING", "PENDING_VERSION", "WAITING_FOR_DEPLOY", ] satisfies TaskRunStatus[]; -export type PENDING_STATUSES = (typeof PENDING_STATUSES)[number]; +type PENDING_STATUSES = (typeof PENDING_STATUSES)[number]; export const FINAL_ATTEMPT_STATUSES = [ "FAILED", @@ -43,15 +43,15 @@ export const FINAL_ATTEMPT_STATUSES = [ export type FINAL_ATTEMPT_STATUSES = (typeof FINAL_ATTEMPT_STATUSES)[number]; -export const NON_FINAL_ATTEMPT_STATUSES = [ +const NON_FINAL_ATTEMPT_STATUSES = [ "PENDING", "EXECUTING", "PAUSED", ] satisfies TaskRunAttemptStatus[]; -export type NON_FINAL_ATTEMPT_STATUSES = (typeof NON_FINAL_ATTEMPT_STATUSES)[number]; +type NON_FINAL_ATTEMPT_STATUSES = (typeof NON_FINAL_ATTEMPT_STATUSES)[number]; -export const FAILED_RUN_STATUSES = [ +const FAILED_RUN_STATUSES = [ "INTERRUPTED", "COMPLETED_WITH_ERRORS", "SYSTEM_FAILURE", @@ -59,25 +59,9 @@ export const FAILED_RUN_STATUSES = [ "TIMED_OUT", ] satisfies TaskRunStatus[]; -export type FAILED_RUN_STATUSES = (typeof FAILED_RUN_STATUSES)[number]; +type FAILED_RUN_STATUSES = (typeof FAILED_RUN_STATUSES)[number]; -export const FATAL_RUN_STATUSES = ["SYSTEM_FAILURE", "CRASHED"] satisfies TaskRunStatus[]; - -export type FATAL_RUN_STATUSES = (typeof FAILED_RUN_STATUSES)[number]; - -export const CANCELLABLE_RUN_STATUSES = NON_FINAL_RUN_STATUSES; -export const CANCELLABLE_ATTEMPT_STATUSES = NON_FINAL_ATTEMPT_STATUSES; - -export const CRASHABLE_RUN_STATUSES = NON_FINAL_RUN_STATUSES; -export const CRASHABLE_ATTEMPT_STATUSES = NON_FINAL_ATTEMPT_STATUSES; - -export const FAILABLE_RUN_STATUSES = NON_FINAL_RUN_STATUSES; - -export const FREEZABLE_RUN_STATUSES: TaskRunStatus[] = ["EXECUTING", "RETRYING_AFTER_FAILURE"]; -export const FREEZABLE_ATTEMPT_STATUSES: TaskRunAttemptStatus[] = ["EXECUTING", "FAILED"]; - -export const RESTORABLE_RUN_STATUSES: TaskRunStatus[] = ["WAITING_TO_RESUME"]; -export const RESTORABLE_ATTEMPT_STATUSES: TaskRunAttemptStatus[] = ["PAUSED"]; +const CANCELLABLE_RUN_STATUSES = NON_FINAL_RUN_STATUSES; export function isFinalRunStatus(status: TaskRunStatus): boolean { return FINAL_RUN_STATUSES.includes(status); @@ -90,46 +74,14 @@ export function isFailedRunStatus(status: TaskRunStatus): boolean { return FAILED_RUN_STATUSES.includes(status); } -export function isFatalRunStatus(status: TaskRunStatus): boolean { - return FATAL_RUN_STATUSES.includes(status); -} - export function isCancellableRunStatus(status: TaskRunStatus): boolean { return CANCELLABLE_RUN_STATUSES.includes(status); } -export function isCancellableAttemptStatus(status: TaskRunAttemptStatus): boolean { - return CANCELLABLE_ATTEMPT_STATUSES.includes(status); -} export function isPendingRunStatus(status: TaskRunStatus): boolean { return PENDING_STATUSES.includes(status); } -export function isCrashableRunStatus(status: TaskRunStatus): boolean { - return CRASHABLE_RUN_STATUSES.includes(status); -} -export function isCrashableAttemptStatus(status: TaskRunAttemptStatus): boolean { - return CRASHABLE_ATTEMPT_STATUSES.includes(status); -} - -export function isFailableRunStatus(status: TaskRunStatus): boolean { - return FAILABLE_RUN_STATUSES.includes(status); -} - -export function isFreezableRunStatus(status: TaskRunStatus): boolean { - return FREEZABLE_RUN_STATUSES.includes(status); -} -export function isFreezableAttemptStatus(status: TaskRunAttemptStatus): boolean { - return FREEZABLE_ATTEMPT_STATUSES.includes(status); -} - -export function isRestorableRunStatus(status: TaskRunStatus): boolean { - return RESTORABLE_RUN_STATUSES.includes(status); -} -export function isRestorableAttemptStatus(status: TaskRunAttemptStatus): boolean { - return RESTORABLE_ATTEMPT_STATUSES.includes(status); -} - export function shouldIdempotencyKeyBeCleared(status: TaskRunStatus): boolean { return isFailedRunStatus(status) || status === "EXPIRED"; } diff --git a/apps/webapp/app/v3/tracer.server.ts b/apps/webapp/app/v3/tracer.server.ts index 17e6a16f7f7..cbf9a937c03 100644 --- a/apps/webapp/app/v3/tracer.server.ts +++ b/apps/webapp/app/v3/tracer.server.ts @@ -24,7 +24,7 @@ import { W3CTraceContextPropagator, } from "@opentelemetry/core"; import sentryRemix from "@sentry/remix"; -import { logs, SeverityNumber } from "@opentelemetry/api-logs"; +import { logs } from "@opentelemetry/api-logs"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs"; @@ -65,12 +65,11 @@ import { singleton } from "~/utils/singleton"; import { LoggerSpanExporter } from "./telemetry/loggerExporter.server"; import { CompactMetricExporter } from "./telemetry/compactMetricExporter.server"; import { logger } from "~/services/logger.server"; -import { flattenAttributes } from "@trigger.dev/core/v3"; import { metricsRegister } from "~/metrics.server"; import { collectDatabaseClientMetrics } from "~/utils/databaseMetrics.server"; import { performance } from "node:perf_hooks"; -export const SEMINTATTRS_FORCE_RECORDING = "forceRecording"; +const SEMINTATTRS_FORCE_RECORDING = "forceRecording"; export const DATASOURCE_CONTEXT_KEY = createContextKey("trigger.db.datasource"); @@ -150,12 +149,9 @@ class NonInheritingTraceContextPropagator implements TextMapPropagator { } } -export const { - tracer, - logger: otelLogger, - provider, - meter, -} = singleton("opentelemetry", setupTelemetry); +const telemetry = singleton("opentelemetry", setupTelemetry); + +export const { tracer, provider, meter } = telemetry; export async function startActiveSpan( name: string, @@ -188,38 +184,6 @@ export async function startActiveSpan( }); } -export async function emitDebugLog(message: string, params: Record = {}) { - otelLogger.emit({ - severityNumber: SeverityNumber.DEBUG, - body: message, - attributes: { ...flattenAttributes(params, "params") }, - }); -} - -export async function emitInfoLog(message: string, params: Record = {}) { - otelLogger.emit({ - severityNumber: SeverityNumber.INFO, - body: message, - attributes: { ...flattenAttributes(params, "params") }, - }); -} - -export async function emitErrorLog(message: string, params: Record = {}) { - otelLogger.emit({ - severityNumber: SeverityNumber.ERROR, - body: message, - attributes: { ...flattenAttributes(params, "params") }, - }); -} - -export async function emitWarnLog(message: string, params: Record = {}) { - otelLogger.emit({ - severityNumber: SeverityNumber.WARN, - body: message, - attributes: { ...flattenAttributes(params, "params") }, - }); -} - function getResource() { const detectors: ResourceDetector[] = [serviceInstanceIdDetector]; diff --git a/apps/webapp/app/v3/tracing.server.ts b/apps/webapp/app/v3/tracing.server.ts index 1074b3d9380..c1b8353004a 100644 --- a/apps/webapp/app/v3/tracing.server.ts +++ b/apps/webapp/app/v3/tracing.server.ts @@ -1,8 +1,5 @@ import type { Span, SpanOptions, Tracer } from "@opentelemetry/api"; import { SpanKind, SpanStatusCode } from "@opentelemetry/api"; -import type { Logger } from "@opentelemetry/api-logs"; -import { SeverityNumber } from "@opentelemetry/api-logs"; -import { flattenAttributes } from "@trigger.dev/core/v3/utils/flattenAttributes"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { attributesFromAuthenticatedEnv } from "./tracer.server"; @@ -52,51 +49,3 @@ export async function startSpanWithEnv( kind: SpanKind.SERVER, }); } - -export async function emitDebugLog( - logger: Logger, - message: string, - params: Record = {} -) { - logger.emit({ - severityNumber: SeverityNumber.DEBUG, - body: message, - attributes: { ...flattenAttributes(params, "params") }, - }); -} - -export async function emitInfoLog( - logger: Logger, - message: string, - params: Record = {} -) { - logger.emit({ - severityNumber: SeverityNumber.INFO, - body: message, - attributes: { ...flattenAttributes(params, "params") }, - }); -} - -export async function emitErrorLog( - logger: Logger, - message: string, - params: Record = {} -) { - logger.emit({ - severityNumber: SeverityNumber.ERROR, - body: message, - attributes: { ...flattenAttributes(params, "params") }, - }); -} - -export async function emitWarnLog( - logger: Logger, - message: string, - params: Record = {} -) { - logger.emit({ - severityNumber: SeverityNumber.WARN, - body: message, - attributes: { ...flattenAttributes(params, "params") }, - }); -} diff --git a/apps/webapp/app/v3/utils/calculateNextSchedule.server.ts b/apps/webapp/app/v3/utils/calculateNextSchedule.server.ts index 9be995e4aa1..5b645807580 100644 --- a/apps/webapp/app/v3/utils/calculateNextSchedule.server.ts +++ b/apps/webapp/app/v3/utils/calculateNextSchedule.server.ts @@ -4,7 +4,7 @@ export function calculateNextScheduledTimestampFromNow(schedule: string, timezon return calculateNextScheduledTimestamp(schedule, timezone, new Date()); } -export function calculateNextScheduledTimestamp( +function calculateNextScheduledTimestamp( schedule: string, timezone: string | null, currentDate: Date = new Date() diff --git a/apps/webapp/app/v3/utils/maxDuration.ts b/apps/webapp/app/v3/utils/maxDuration.ts index b19d2786fd5..d456936ae35 100644 --- a/apps/webapp/app/v3/utils/maxDuration.ts +++ b/apps/webapp/app/v3/utils/maxDuration.ts @@ -4,19 +4,3 @@ const MAXIMUM_MAX_DURATION = 2_147_483_647; // largest 32-bit signed integer export function clampMaxDuration(maxDuration: number): number { return Math.min(Math.max(maxDuration, MINIMUM_MAX_DURATION), MAXIMUM_MAX_DURATION); } - -export function getMaxDuration( - maxDuration?: number | null, - defaultMaxDuration?: number | null -): number | undefined { - if (!maxDuration) { - return defaultMaxDuration ?? undefined; - } - - // Setting the maxDuration to MAXIMUM_MAX_DURATION means we don't want to use the default maxDuration - if (maxDuration === MAXIMUM_MAX_DURATION) { - return; - } - - return maxDuration; -} diff --git a/apps/webapp/app/v3/vercel/vercelOAuthState.server.ts b/apps/webapp/app/v3/vercel/vercelOAuthState.server.ts index e6bfbb0362b..0bdea110209 100644 --- a/apps/webapp/app/v3/vercel/vercelOAuthState.server.ts +++ b/apps/webapp/app/v3/vercel/vercelOAuthState.server.ts @@ -2,7 +2,7 @@ import { generateJWT, validateJWT } from "@trigger.dev/core/v3/jwt"; import { z } from "zod"; import { env } from "~/env.server"; -export const VercelOAuthStateSchema = z.object({ +const VercelOAuthStateSchema = z.object({ organizationId: z.string(), projectId: z.string(), environmentSlug: z.string(), diff --git a/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts b/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts index 1399b87fc2b..2d4d57b33f4 100644 --- a/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts +++ b/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts @@ -1,7 +1,7 @@ import { Result } from "neverthrow"; import { z } from "zod"; -export const EnvSlugSchema = z.enum(["dev", "stg", "prod", "preview"]); +const EnvSlugSchema = z.enum(["dev", "stg", "prod", "preview"]); export type EnvSlug = z.infer; export const ALL_ENV_SLUGS: EnvSlug[] = ["dev", "stg", "prod", "preview"]; @@ -15,16 +15,6 @@ const safeJsonParse = Result.fromThrowable( * Zod transform for form fields that submit JSON-encoded arrays. * Parses the string as JSON and returns the array, or null if invalid. */ -export const jsonArrayField = z - .string() - .optional() - .transform((val) => { - if (!val) return null; - return safeJsonParse(val).match( - (parsed) => (Array.isArray(parsed) ? parsed : null), - () => null - ); - }); /** * Zod transform for form fields that submit JSON-encoded EnvSlug arrays. @@ -45,7 +35,7 @@ export const envSlugArrayField = z ); }); -export const VercelIntegrationConfigSchema = z.object({ +const VercelIntegrationConfigSchema = z.object({ atomicBuilds: z.array(EnvSlugSchema).nullable().optional(), pullEnvVarsBeforeBuild: z.array(EnvSlugSchema).nullable().optional(), /** Maps a custom Vercel environment to Trigger.dev's staging environment. */ @@ -70,7 +60,7 @@ export type TriggerEnvironmentType = z.infer; * Missing env slug = sync all vars. Missing var in env = sync by default. * Only explicitly `false` entries disable sync. */ -export const SyncEnvVarsMappingSchema = z +const SyncEnvVarsMappingSchema = z .record(EnvSlugSchema, z.record(z.string(), z.boolean())) .default({}); @@ -151,17 +141,6 @@ export function getAvailableEnvSlugsForBuildSettings( ); } -export function isDiscoverEnvVarsEnabledForEnvironment( - discoverEnvVars: EnvSlug[] | null | undefined, - environmentType: TriggerEnvironmentType -): boolean { - if (!discoverEnvVars || discoverEnvVars.length === 0) { - return false; - } - const envSlug = envTypeToSlug(environmentType); - return discoverEnvVars.includes(envSlug); -} - export function envTypeToSlug(environmentType: TriggerEnvironmentType): EnvSlug { switch (environmentType) { case "DEVELOPMENT": @@ -237,14 +216,3 @@ export function isPullEnvVarsEnabledForEnvironment( const envSlug = envTypeToSlug(environmentType); return pullEnvVarsBeforeBuild.includes(envSlug); } - -export function isAtomicBuildsEnabledForEnvironment( - atomicBuilds: EnvSlug[] | null | undefined, - environmentType: TriggerEnvironmentType -): boolean { - if (!atomicBuilds || atomicBuilds.length === 0) { - return false; - } - const envSlug = envTypeToSlug(environmentType); - return atomicBuilds.includes(envSlug); -} diff --git a/apps/webapp/app/v3/webhookEngine.server.ts b/apps/webapp/app/v3/webhookEngine.server.ts index d58a89919b9..b06f2f0024b 100644 --- a/apps/webapp/app/v3/webhookEngine.server.ts +++ b/apps/webapp/app/v3/webhookEngine.server.ts @@ -27,8 +27,6 @@ import { meter, tracer } from "./tracer.server"; export const webhookEngine = singleton("WebhookEngine", createWebhookEngine); -export type { WebhookEngine }; - // The plaintext signing secret is stored under the "DATABASE" SecretStore // provider as { secret: string } (same shape as environment variables). const SigningSecretSchema = z.object({ secret: z.string() }); diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 28abe146de6..8e232b14795 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -163,8 +163,6 @@ "isbot": "^3.6.5", "jose": "^5.4.0", "json-stable-stringify": "^1.3.0", - "jsonpointer": "^5.0.1", - "lodash.omit": "^4.5.0", "lru-cache": "^11.2.4", "lucide-react": "^0.229.0", "marked": "^4.0.18", @@ -186,7 +184,6 @@ "prism-react-renderer": "^2.3.1", "prismjs": "^1.30.0", "prom-client": "^15.1.0", - "prop-types": "^15.8.1", "qrcode.react": "^4.2.0", "random-words": "^2.0.0", "react": "^18.2.0", @@ -233,47 +230,32 @@ "@internal/testcontainers": "workspace:*", "@playwright/test": "^1.36.2", "@remix-run/dev": "2.17.5", - "@remix-run/testing": "^2.17.5", "@sentry/cli": "2.50.2", - "@swc/core": "^1.3.4", - "@swc/helpers": "^0.4.11", "@tailwindcss/forms": "^0.5.11", "@tailwindcss/postcss": "^4.3.1", "@tailwindcss/typography": "^0.5.20", "@testcontainers/postgresql": "^11.14.0", "@total-typescript/ts-reset": "^0.4.2", - "@types/bcryptjs": "^2.4.2", "@types/compression": "^1.7.2", "@types/cookie": "^0.6.0", "@types/express": "^4.17.13", - "@types/json-query": "^2.2.3", "@types/marked": "^4.0.3", "@types/morgan": "^1.9.3", - "@types/node-fetch": "^2.6.2", "@types/pg": "^8.11.10", "@types/prismjs": "^1.26.0", - "@types/qs": "^6.9.7", "@types/react": "18.2.69", "@types/react-dom": "18.2.7", "@types/regression": "^2.0.6", "@types/semver": "^7.5.0", "@types/slug": "^5.0.3", "@types/supertest": "^6.0.2", - "@types/tar": "^6.1.4", "@types/ws": "^8.5.3", "autoevals": "^0.0.130", - "css-loader": "^6.10.0", - "datepicker": "link:@types/@react-aria/datepicker", "engine.io": "^6.6.7", "esbuild": "^0.15.10", "evalite": "1.0.0-beta.16", - "postcss-import": "^16.0.1", - "postcss-loader": "^8.1.1", - "rimraf": "^6.0.1", - "style-loader": "^3.3.4", "supertest": "^7.0.0", "tailwind-scrollbar": "^4.0.2", - "tsconfig-paths": "^3.14.1", "tsx": "^4.20.6", "typescript": "catalog:", "typescript-legacy-api": "npm:typescript@6.0.3", diff --git a/apps/webapp/test/otlpMetrics.helpers.ts b/apps/webapp/test/otlpMetrics.helpers.ts index 7141d85e7e7..e1e7549765b 100644 --- a/apps/webapp/test/otlpMetrics.helpers.ts +++ b/apps/webapp/test/otlpMetrics.helpers.ts @@ -8,7 +8,7 @@ export async function latestMetrics(helper: MetricsHelper) { return all[all.length - 1]; } -export function findMetric(resourceMetrics: any, name: string): any | undefined { +function findMetric(resourceMetrics: any, name: string): any | undefined { if (!resourceMetrics) return undefined; for (const scopeMetrics of resourceMetrics.scopeMetrics) { for (const metric of scopeMetrics.metrics) { diff --git a/apps/webapp/test/setup-test-env.ts b/apps/webapp/test/setup-test-env.ts deleted file mode 100644 index 48fcc4317a5..00000000000 --- a/apps/webapp/test/setup-test-env.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { installGlobals } from "@remix-run/node"; -import "@testing-library/jest-dom/extend-expect"; - -installGlobals(); diff --git a/apps/webapp/test/utils/streams.ts b/apps/webapp/test/utils/streams.ts deleted file mode 100644 index 79249b4d6c8..00000000000 --- a/apps/webapp/test/utils/streams.ts +++ /dev/null @@ -1,46 +0,0 @@ -export async function convertResponseStreamToArray(response: Response): Promise { - return convertReadableStreamToArray(response.body!.pipeThrough(new TextDecoderStream())); -} - -export async function convertResponseSSEStreamToArray(response: Response): Promise { - const parseSSEDataTransform = new TransformStream({ - async transform(chunk, controller) { - for (const line of chunk.split("\n")) { - if (line.startsWith("data:")) { - controller.enqueue(line.slice(6)); - } - } - }, - }); - - return convertReadableStreamToArray( - response.body!.pipeThrough(new TextDecoderStream()).pipeThrough(parseSSEDataTransform) - ); -} - -export async function convertReadableStreamToArray(stream: ReadableStream): Promise { - const reader = stream.getReader(); - const result: T[] = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - result.push(value); - } - - return result; -} - -export function convertArrayToReadableStream(values: T[]): ReadableStream { - return new ReadableStream({ - start(controller) { - try { - for (const value of values) { - controller.enqueue(value); - } - } finally { - controller.close(); - } - }, - }); -} diff --git a/internal-packages/cache/package.json b/internal-packages/cache/package.json index 8d1bec36c77..939bc298be9 100644 --- a/internal-packages/cache/package.json +++ b/internal-packages/cache/package.json @@ -7,7 +7,6 @@ "type": "module", "dependencies": { "@internal/redis": "workspace:*", - "@trigger.dev/core": "workspace:*", "@unkey/cache": "^1.5.0", "@unkey/error": "^0.2.0", "lru-cache": "^11.2.4", diff --git a/internal-packages/clickhouse/src/client/errors.ts b/internal-packages/clickhouse/src/client/errors.ts index 906aa87e13d..ff0be4d0d54 100644 --- a/internal-packages/clickhouse/src/client/errors.ts +++ b/internal-packages/clickhouse/src/client/errors.ts @@ -1,6 +1,6 @@ -export type ErrorContext = Record; +type ErrorContext = Record; -export abstract class BaseError extends Error { +abstract class BaseError extends Error { public abstract readonly retry: boolean; public readonly cause: BaseError | undefined; public readonly context: TContext | undefined; diff --git a/internal-packages/clickhouse/src/client/tsql.ts b/internal-packages/clickhouse/src/client/tsql.ts index f61009237e4..e54051fc364 100644 --- a/internal-packages/clickhouse/src/client/tsql.ts +++ b/internal-packages/clickhouse/src/client/tsql.ts @@ -27,7 +27,7 @@ const logger = new Logger("tsql", "info"); export type { QueryStats }; -export type { FieldMappings, QuerySettings, TableSchema, TimeRange, WhereClauseCondition }; +export type { FieldMappings, TableSchema, WhereClauseCondition }; /** * Options for executing a TSQL query diff --git a/internal-packages/dashboard-agent/src/compaction.ts b/internal-packages/dashboard-agent/src/compaction.ts index 83e5d82b830..68001b00ba4 100644 --- a/internal-packages/dashboard-agent/src/compaction.ts +++ b/internal-packages/dashboard-agent/src/compaction.ts @@ -74,7 +74,7 @@ Write a summary in under 400 words, as notes rather than prose. Keep, in this or Drop tool mechanics, retries, and anything already superseded. Do not add advice, and do not invent anything that is not in the transcript. Everything you write is a record of what the transcript said, not a claim about the present.`; /** A summary that reads as a summary, and never as the user's next question. */ -export function summaryMessage(summary: string, durableState?: string): ModelMessage { +function summaryMessage(summary: string, durableState?: string): ModelMessage { return { role: "user", content: durableState @@ -116,7 +116,7 @@ export function shouldCompactConversation(event: { * The state a summary may not swallow * ------------------------------------------------------------------ */ -export type PinnedInvestigation = { +type PinnedInvestigation = { id: string; title: string; outcome: string; diff --git a/internal-packages/dashboard-agent/src/eval-policy.ts b/internal-packages/dashboard-agent/src/eval-policy.ts index c17399c9c3e..24b95f2eaab 100644 --- a/internal-packages/dashboard-agent/src/eval-policy.ts +++ b/internal-packages/dashboard-agent/src/eval-policy.ts @@ -24,7 +24,7 @@ export const DEFAULT_EVAL_SAMPLE_RATE = 0.1; export const DEFAULT_CI_EVAL_SAMPLE_RATE = 1; /** Set to "ci" by the golden harness only. Nothing else selects the CI lane. */ -export const EVAL_CONTEXT_ENV = "DASHBOARD_AGENT_EVAL_CONTEXT"; +const EVAL_CONTEXT_ENV = "DASHBOARD_AGENT_EVAL_CONTEXT"; /** * The two lanes read different variables, so a CI run can neither read nor change the @@ -60,7 +60,7 @@ export function shouldEvalTurn(): boolean { * would have to be handed the customer's code to check the answer against it, and a * source-free judgement of a source-grounded answer is not worth the row. */ -export const SOURCE_TOOLS = ["read_file", "search_code", "list_files", "get_repo_info"]; +const SOURCE_TOOLS = ["read_file", "search_code", "list_files", "get_repo_info"]; export function turnReadSource(toolActivity: Array<{ toolName: string }>): boolean { return toolActivity.some((activity) => SOURCE_TOOLS.includes(activity.toolName)); @@ -369,7 +369,7 @@ export function classifyEvalError(output: unknown): EvalErrorCategory { * already there. A string `error` is replaced by its shape: the label is what the judge * gets, never the sentence it came from. */ -export function annotateEvalErrorCategory(original: unknown, redacted: unknown): unknown { +function annotateEvalErrorCategory(original: unknown, redacted: unknown): unknown { if (!evalOutputErrored(original)) return redacted; if (redacted === null || typeof redacted !== "object" || Array.isArray(redacted)) return redacted; diff --git a/internal-packages/dashboard-agent/src/repo-tools.ts b/internal-packages/dashboard-agent/src/repo-tools.ts index e7e5032a0f1..e02c91dba05 100644 --- a/internal-packages/dashboard-agent/src/repo-tools.ts +++ b/internal-packages/dashboard-agent/src/repo-tools.ts @@ -49,7 +49,7 @@ const FETCH_TIMEOUT_MS = 30_000; // Points at the mechanism the prompt already teaches — the stack-trace line is // where a truncated read gets resumed. -export const READ_TRUNCATION_NOTICE = +const READ_TRUNCATION_NOTICE = `Truncated to the first ${MAX_READ_LINES} lines / ${MAX_READ_BYTES / 1024}KB. ` + "Read the part you need with startLine and endLine — the line from the stack trace or the search match is where to start."; diff --git a/internal-packages/dashboard-agent/src/tool-api-client.ts b/internal-packages/dashboard-agent/src/tool-api-client.ts index 2458f31c792..1cb2f68ae21 100644 --- a/internal-packages/dashboard-agent/src/tool-api-client.ts +++ b/internal-packages/dashboard-agent/src/tool-api-client.ts @@ -45,7 +45,7 @@ const QUERY_TIMEOUT_MS = 30_000; // "query" is the server rejecting the TRQL, "transport" is the request breaking, "busy" is // the server too loaded or rate limited to answer — the same query may work shortly. Chart // validation only fails a render on "query". -export type QueryPostResult = +type QueryPostResult = | { ok: true; rows: Array> } | { ok: false; kind: "query" | "transport" | "busy"; error: string }; diff --git a/internal-packages/dashboard-agent/src/tool-api.ts b/internal-packages/dashboard-agent/src/tool-api.ts index e0376015080..ed66b5be334 100644 --- a/internal-packages/dashboard-agent/src/tool-api.ts +++ b/internal-packages/dashboard-agent/src/tool-api.ts @@ -53,7 +53,7 @@ import type { InvestigationRenderer } from "./tool-investigations"; * environment is stated as one; a failed exchange says the read didn't land, and carries * its status, so an authorization failure is never reported as an absent environment. */ -export function envUnavailableError(result: EnvUnavailable, action: string): { error: string } { +function envUnavailableError(result: EnvUnavailable, action: string): { error: string } { if (result.envUnavailable === "missing") { return { error: `No current environment is available to ${action}.` }; } diff --git a/internal-packages/dashboard-agent/src/tool-evidence.ts b/internal-packages/dashboard-agent/src/tool-evidence.ts index c9636c7795b..78a5c0535c4 100644 --- a/internal-packages/dashboard-agent/src/tool-evidence.ts +++ b/internal-packages/dashboard-agent/src/tool-evidence.ts @@ -15,7 +15,7 @@ export type EvidenceScope = { projectRef: string; environmentId: string }; * Builds the canonical `trigger://` URI for a cited ref. A ref that can't be * canonicalized is returned as a named error, never dropped. */ -export function canonicalizeEvidence( +function canonicalizeEvidence( items: EvidenceRef[], scope: EvidenceScope, reads: SourceReadLookup diff --git a/internal-packages/dashboard-agent/src/tool-investigations.ts b/internal-packages/dashboard-agent/src/tool-investigations.ts index 43c8dd4589d..f9c9292f060 100644 --- a/internal-packages/dashboard-agent/src/tool-investigations.ts +++ b/internal-packages/dashboard-agent/src/tool-investigations.ts @@ -47,7 +47,7 @@ const RECURRENCE_WATCH = { checkEveryMinutes: 15, maxHours: WATCH_MAX_HOURS } as * The card's typed next actions, decided here and never by the model. "Show code" * needs a concluded card, a cited source line, and a read at that commit this turn. */ -export function investigationCapabilities( +function investigationCapabilities( state: InvestigationState, reads: SourceReadLookup ): InvestigationCapabilities | null { @@ -119,7 +119,7 @@ export function investigationCapabilities( return { version: INVESTIGATION_CAPABILITIES_VERSION, actions }; } -export type InvestigationRenderResult = +type InvestigationRenderResult = | { error: string } | { blocks: unknown[]; investigationId?: string; revision?: number }; diff --git a/internal-packages/dashboard-agent/src/tools.ts b/internal-packages/dashboard-agent/src/tools.ts index 24050a3f9ea..0282e8fa661 100644 --- a/internal-packages/dashboard-agent/src/tools.ts +++ b/internal-packages/dashboard-agent/src/tools.ts @@ -10,9 +10,7 @@ import { buildWatchTools } from "./watch-tools"; import type { DashboardAgentToolContext } from "./tool-context"; export type { DashboardAgentToolContext } from "./tool-context"; -export type { InvestigationsCapability } from "./tool-investigations"; export { showCodeAskPrompt } from "./tool-investigations"; -export { getReportModelOutput, renderViewModelOutput } from "./tool-curation"; /** * Assembles the ready adapters into one tool set. The key order below is frozen: diff --git a/internal-packages/dashboard-agent/src/watch-delivery.ts b/internal-packages/dashboard-agent/src/watch-delivery.ts index 64c092caf99..45640e8dab1 100644 --- a/internal-packages/dashboard-agent/src/watch-delivery.ts +++ b/internal-packages/dashboard-agent/src/watch-delivery.ts @@ -42,7 +42,7 @@ export type WatchTickStore = { }): Promise<{ tickCount: number; lastCheckedAt: Date | null } | null>; }; -export type WatchWakeAck = { appended: boolean }; +type WatchWakeAck = { appended: boolean }; export type WatchDeliveryDeps = { store: Pick< diff --git a/internal-packages/dashboard-agent/src/watch-lifecycle.ts b/internal-packages/dashboard-agent/src/watch-lifecycle.ts index 2e55dc38018..85a94e82a26 100644 --- a/internal-packages/dashboard-agent/src/watch-lifecycle.ts +++ b/internal-packages/dashboard-agent/src/watch-lifecycle.ts @@ -62,7 +62,7 @@ export const REVOKED_CODES = new Set(["access_revoked", "cancelled", "not_found" * failures replaces one another instead of nesting — the row's `lastResult` reaches the * wake facts, the alert and the webhook body. */ -export function lastObservedResult(lastResult: unknown): Record | undefined { +function lastObservedResult(lastResult: unknown): Record | undefined { let current = lastResult; while (isCheckFailure(current)) current = current.previous; return current !== null && typeof current === "object" && !Array.isArray(current) diff --git a/internal-packages/dashboard-agent/src/watch-tick.ts b/internal-packages/dashboard-agent/src/watch-tick.ts index e4821de205d..739a5f75bfb 100644 --- a/internal-packages/dashboard-agent/src/watch-tick.ts +++ b/internal-packages/dashboard-agent/src/watch-tick.ts @@ -22,24 +22,14 @@ import { * `watchBatchTick` for a group. The webapp evaluates conditions; a tick records them. */ -export type { - WatchDeliveryDeps, - WatchTickOutcome, - WatchTickResult, - WatchTickStore, -} from "./watch-delivery"; -export { expiredFacts, resolveAndDeliver } from "./watch-delivery"; -export type { CheckOutcome, WatchLifecycleDeps } from "./watch-lifecycle"; -export { runWatchLifecycle } from "./watch-lifecycle"; +export type { WatchTickResult, WatchTickStore } from "./watch-delivery"; export type { WatchBatchCheckEntry, WatchBatchCheckResponse, WatchBatchTickDeps, WatchBatchTickPayload, - WatchBatchTickResult, } from "./watch-batch"; export { runWatchBatchTick } from "./watch-batch"; -export { appendWakeToSession, getWatchDb } from "./watch-task-adapters"; export type WatchTickPayload = { watchId: string; diff --git a/internal-packages/database/package.json b/internal-packages/database/package.json index 9cff6d17870..b6e96c62fc7 100644 --- a/internal-packages/database/package.json +++ b/internal-packages/database/package.json @@ -10,7 +10,6 @@ "prisma": "6.14.0" }, "devDependencies": { - "@types/decimal.js": "^7.4.3", "rimraf": "6.0.1", "vitest": "4.1.7" }, diff --git a/internal-packages/emails/emails/components/styles.ts b/internal-packages/emails/emails/components/styles.ts index 7e7db866210..e5f0f29de66 100644 --- a/internal-packages/emails/emails/components/styles.ts +++ b/internal-packages/emails/emails/components/styles.ts @@ -20,10 +20,6 @@ export const container = { marginBottom: "64px", }; -export const box = { - padding: "0 48px", -}; - export const hr = { borderColor: "#272A2E", margin: "20px 0", @@ -34,15 +30,6 @@ export const sans = { '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', }; -export const paragraph = { - color: "#878C99", - fontFamily: - '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', - fontSize: "16px", - lineHeight: "24px", - textAlign: "left" as const, -}; - export const paragraphLight = { color: "#D7D9DD", fontFamily: @@ -83,19 +70,6 @@ export const anchor = { textDecoration: "underline", }; -export const button = { - backgroundColor: "#826DFF", - borderRadius: "5px", - color: "#D7D9DD", - fontFamily: - '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', - fontSize: "16px", - fontWeight: "bold", - textDecoration: "none", - textAlign: "center" as const, - display: "block", -}; - export const footer = { color: "#878C99", fontFamily: diff --git a/internal-packages/emails/package.json b/internal-packages/emails/package.json index 33dfe4c1d71..38ca8988a63 100644 --- a/internal-packages/emails/package.json +++ b/internal-packages/emails/package.json @@ -17,7 +17,6 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "resend": "^3.2.0", - "tiny-invariant": "^1.2.0", "zod": "3.25.76" }, "devDependencies": { diff --git a/internal-packages/observability-map/src/mutations.ts b/internal-packages/observability-map/src/mutations.ts index 0a273137e9e..55d57468874 100644 --- a/internal-packages/observability-map/src/mutations.ts +++ b/internal-packages/observability-map/src/mutations.ts @@ -6,13 +6,13 @@ import ts from "@typescript/typescript6"; * is tracked separately in `ADDITIVE_IDS`: INTERNALS.md, "The mutation harness". */ -export type MutationKind = "preserving" | "deleting"; +type MutationKind = "preserving" | "deleting"; /** * The new source, and how many places in it the rewrite landed. `sites` is what the anti-vacuity guard * reads, because a file count says nothing about whether the rewrite reached anything inside the file. */ -export type MutationResult = { source: string; sites: number }; +type MutationResult = { source: string; sites: number }; export type Mutation = { id: string; diff --git a/internal-packages/observability-map/src/report/prComment.ts b/internal-packages/observability-map/src/report/prComment.ts index abe5c3dfb37..b72d6d838af 100644 --- a/internal-packages/observability-map/src/report/prComment.ts +++ b/internal-packages/observability-map/src/report/prComment.ts @@ -10,7 +10,7 @@ import { } from "./terminal.js"; /** First line of every comment this job posts, so the upsert step can find its own comment again. */ -export const MARKER = ""; +const MARKER = ""; /** * The commit a comment was rendered for. Data rather than something the renderers read for themselves, diff --git a/internal-packages/observability-map/src/report/terminal.ts b/internal-packages/observability-map/src/report/terminal.ts index 5b0df7849ac..0f45d2ead8c 100644 --- a/internal-packages/observability-map/src/report/terminal.ts +++ b/internal-packages/observability-map/src/report/terminal.ts @@ -33,7 +33,7 @@ export const fixFirst = (entries: ScoredEntry[]): ScoredEntry[] => /** An entry whose only finding is `request-context`, which fails almost everything, so it is * collapsed into the `CONTEXT` figure rather than listed. An entry that fails something else as well * keeps all of its findings and stays in the list. */ -export const contextOnly = (e: ScoredEntry) => { +const contextOnly = (e: ScoredEntry) => { const failures = scoredFailures(e); return failures.length === 1 && failures[0]!.id === "request-context"; }; diff --git a/internal-packages/observability-map/src/score.ts b/internal-packages/observability-map/src/score.ts index c045e216f0e..2b0de0d10b2 100644 --- a/internal-packages/observability-map/src/score.ts +++ b/internal-packages/observability-map/src/score.ts @@ -34,7 +34,7 @@ export type ScoredEntry = { * What one check contributes to the composite. Disclosed rather than weighted, deliberately: see * README, "What the score is made of". */ -export type CheckContribution = { +type CheckContribution = { id: string; /** Entry points the check was applicable to, pre-suppression. */ applicable: number; diff --git a/internal-packages/observability-map/src/sensitivity.ts b/internal-packages/observability-map/src/sensitivity.ts index 5ef6582c849..5f85b1f875d 100644 --- a/internal-packages/observability-map/src/sensitivity.ts +++ b/internal-packages/observability-map/src/sensitivity.ts @@ -17,15 +17,12 @@ export const SENSITIVE_SYMBOLS = [ "createPersonalAccessToken", "createPersonalAccessTokenFromAuthorizationCode", "revokePersonalAccessToken", - "createOrganizationAccessToken", - "revokeOrganizationAccessToken", "createAuthorizationCode", "createApiKeyForEnv", "createPkApiKeyForEnv", "regenerateApiKey", "generateJWTTokenForEnvironment", "generateRegistryCredentials", - "mintRunToken", "mintSessionToken", "mintDashboardAgentToken", "mintDashboardAgentUserActorToken", diff --git a/internal-packages/otlp-importer/jest.config.js b/internal-packages/otlp-importer/jest.config.js deleted file mode 100644 index e21cd117ce1..00000000000 --- a/internal-packages/otlp-importer/jest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - moduleFileExtensions: ["ts", "tsx", "js"], - transform: { - "^.+\\.(ts|tsx)$": "ts-jest", - }, - testMatch: ["/test/**/*.ts?(x)", "/test/**/?(*.)+(spec|test).ts?(x)"], - testEnvironment: "node", -}; diff --git a/internal-packages/otlp-importer/package.json b/internal-packages/otlp-importer/package.json index 18f87188ca2..540e84e956c 100644 --- a/internal-packages/otlp-importer/package.json +++ b/internal-packages/otlp-importer/package.json @@ -28,7 +28,6 @@ }, "devDependencies": { "@types/node": "^24.13.3", - "rimraf": "^6.0.1", "ts-proto": "^1.167.3" }, "engines": { diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/logs/v1/logs_service.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/logs/v1/logs_service.ts index 50d82933e00..4cabc5882f5 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/logs/v1/logs_service.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/logs/v1/logs_service.ts @@ -3,7 +3,7 @@ import Long from "long"; import _m0 from "protobufjs/minimal"; import { ResourceLogs } from "../../../logs/v1/logs"; -export const protobufPackage = "opentelemetry.proto.collector.logs.v1"; +const protobufPackage = "opentelemetry.proto.collector.logs.v1"; export interface ExportLogsServiceRequest { /** @@ -280,7 +280,7 @@ export const ExportLogsPartialSuccess = { * OpenTelemetry and an collector, or between an collector and a central collector (in this * case logs are sent/received to/from multiple Applications). */ -export interface LogsService { +interface LogsService { /** * For performance reasons, it is recommended to keep this RPC * alive for the entire life of the application. @@ -288,8 +288,8 @@ export interface LogsService { export(request: ExportLogsServiceRequest): Promise; } -export const LogsServiceServiceName = "opentelemetry.proto.collector.logs.v1.LogsService"; -export class LogsServiceClientImpl implements LogsService { +const LogsServiceServiceName = "opentelemetry.proto.collector.logs.v1.LogsService"; +class LogsServiceClientImpl implements LogsService { private readonly rpc: Rpc; private readonly service: string; constructor(rpc: Rpc, opts?: { service?: string }) { @@ -310,7 +310,7 @@ interface Rpc { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin +type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> @@ -321,7 +321,7 @@ export type DeepPartial = T extends Builtin : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin +type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/metrics/v1/metrics_service.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/metrics/v1/metrics_service.ts index 9f7c913c34b..beab7ef6867 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/metrics/v1/metrics_service.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/metrics/v1/metrics_service.ts @@ -3,7 +3,7 @@ import Long from "long"; import _m0 from "protobufjs/minimal"; import { ResourceMetrics } from "../../../metrics/v1/metrics"; -export const protobufPackage = "opentelemetry.proto.collector.metrics.v1"; +const protobufPackage = "opentelemetry.proto.collector.metrics.v1"; export interface ExportMetricsServiceRequest { /** @@ -290,7 +290,7 @@ export const ExportMetricsPartialSuccess = { * instrumented with OpenTelemetry and a collector, or between a collector and a * central collector. */ -export interface MetricsService { +interface MetricsService { /** * For performance reasons, it is recommended to keep this RPC * alive for the entire life of the application. @@ -298,8 +298,8 @@ export interface MetricsService { export(request: ExportMetricsServiceRequest): Promise; } -export const MetricsServiceServiceName = "opentelemetry.proto.collector.metrics.v1.MetricsService"; -export class MetricsServiceClientImpl implements MetricsService { +const MetricsServiceServiceName = "opentelemetry.proto.collector.metrics.v1.MetricsService"; +class MetricsServiceClientImpl implements MetricsService { private readonly rpc: Rpc; private readonly service: string; constructor(rpc: Rpc, opts?: { service?: string }) { @@ -320,7 +320,7 @@ interface Rpc { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin +type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> @@ -331,7 +331,7 @@ export type DeepPartial = T extends Builtin : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin +type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/trace/v1/trace_service.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/trace/v1/trace_service.ts index ee38c623fab..b79024b2b80 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/trace/v1/trace_service.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/trace/v1/trace_service.ts @@ -3,7 +3,7 @@ import Long from "long"; import _m0 from "protobufjs/minimal"; import { ResourceSpans } from "../../../trace/v1/trace"; -export const protobufPackage = "opentelemetry.proto.collector.trace.v1"; +const protobufPackage = "opentelemetry.proto.collector.trace.v1"; export interface ExportTraceServiceRequest { /** @@ -281,7 +281,7 @@ export const ExportTracePartialSuccess = { * OpenTelemetry and a collector, or between a collector and a central collector (in this * case spans are sent/received to/from multiple Applications). */ -export interface TraceService { +interface TraceService { /** * For performance reasons, it is recommended to keep this RPC * alive for the entire life of the application. @@ -289,8 +289,8 @@ export interface TraceService { export(request: ExportTraceServiceRequest): Promise; } -export const TraceServiceServiceName = "opentelemetry.proto.collector.trace.v1.TraceService"; -export class TraceServiceClientImpl implements TraceService { +const TraceServiceServiceName = "opentelemetry.proto.collector.trace.v1.TraceService"; +class TraceServiceClientImpl implements TraceService { private readonly rpc: Rpc; private readonly service: string; constructor(rpc: Rpc, opts?: { service?: string }) { @@ -311,7 +311,7 @@ interface Rpc { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin +type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> @@ -322,7 +322,7 @@ export type DeepPartial = T extends Builtin : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin +type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/common/v1/common.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/common/v1/common.ts index 2a307a667c6..7c65f6ac761 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/common/v1/common.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/common/v1/common.ts @@ -2,7 +2,7 @@ import Long from "long"; import _m0 from "protobufjs/minimal"; -export const protobufPackage = "opentelemetry.proto.common.v1"; +const protobufPackage = "opentelemetry.proto.common.v1"; /** * AnyValue is used to represent any type of attribute value. AnyValue may contain a @@ -23,7 +23,7 @@ export interface AnyValue { * ArrayValue is a list of AnyValue messages. We need ArrayValue as a message * since oneof in AnyValue does not allow repeated fields. */ -export interface ArrayValue { +interface ArrayValue { /** Array of values. The array may be empty (contain 0 elements). */ values: AnyValue[]; } @@ -247,7 +247,7 @@ function createBaseArrayValue(): ArrayValue { return { values: [] }; } -export const ArrayValue = { +const ArrayValue = { encode(message: ArrayValue, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { for (const v of message.values) { AnyValue.encode(v!, writer.uint32(10).fork()).ldelim(); @@ -579,7 +579,7 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin +type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> @@ -590,7 +590,7 @@ export type DeepPartial = T extends Builtin : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin +type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/logs/v1/logs.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/logs/v1/logs.ts index 2d0b3ebf56a..3c55f9cce70 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/logs/v1/logs.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/logs/v1/logs.ts @@ -4,7 +4,7 @@ import _m0 from "protobufjs/minimal"; import { AnyValue, InstrumentationScope, KeyValue } from "../../common/v1/common"; import { Resource } from "../../resource/v1/resource"; -export const protobufPackage = "opentelemetry.proto.logs.v1"; +const protobufPackage = "opentelemetry.proto.logs.v1"; /** Possible values for LogRecord.SeverityNumber. */ export enum SeverityNumber { @@ -37,7 +37,7 @@ export enum SeverityNumber { UNRECOGNIZED = -1, } -export function severityNumberFromJSON(object: any): SeverityNumber { +function severityNumberFromJSON(object: any): SeverityNumber { switch (object) { case 0: case "SEVERITY_NUMBER_UNSPECIFIED": @@ -121,7 +121,7 @@ export function severityNumberFromJSON(object: any): SeverityNumber { } } -export function severityNumberToJSON(object: SeverityNumber): string { +function severityNumberToJSON(object: SeverityNumber): string { switch (object) { case SeverityNumber.UNSPECIFIED: return "SEVERITY_NUMBER_UNSPECIFIED"; @@ -188,7 +188,7 @@ export function severityNumberToJSON(object: SeverityNumber): string { * * (logRecord.flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK) */ -export enum LogRecordFlags { +enum LogRecordFlags { /** * DO_NOT_USE - The zero value for the enum. Should not be used for comparisons. * Instead use bitwise "and" with the appropriate mask as shown above. @@ -199,7 +199,7 @@ export enum LogRecordFlags { UNRECOGNIZED = -1, } -export function logRecordFlagsFromJSON(object: any): LogRecordFlags { +function logRecordFlagsFromJSON(object: any): LogRecordFlags { switch (object) { case 0: case "LOG_RECORD_FLAGS_DO_NOT_USE": @@ -214,7 +214,7 @@ export function logRecordFlagsFromJSON(object: any): LogRecordFlags { } } -export function logRecordFlagsToJSON(object: LogRecordFlags): string { +function logRecordFlagsToJSON(object: LogRecordFlags): string { switch (object) { case LogRecordFlags.DO_NOT_USE: return "LOG_RECORD_FLAGS_DO_NOT_USE"; @@ -238,7 +238,7 @@ export function logRecordFlagsToJSON(object: LogRecordFlags): string { * When new fields are added into this message, the OTLP request MUST be updated * as well. */ -export interface LogsData { +interface LogsData { /** * An array of ResourceLogs. * For data coming from a single resource this array will typically contain @@ -382,7 +382,7 @@ function createBaseLogsData(): LogsData { return { resourceLogs: [] }; } -export const LogsData = { +const LogsData = { encode(message: LogsData, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { for (const v of message.resourceLogs) { ResourceLogs.encode(v!, writer.uint32(10).fork()).ldelim(); @@ -882,7 +882,7 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin +type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> @@ -893,7 +893,7 @@ export type DeepPartial = T extends Builtin : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin +type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/metrics/v1/metrics.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/metrics/v1/metrics.ts index 0f368d54871..1de89208dee 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/metrics/v1/metrics.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/metrics/v1/metrics.ts @@ -4,7 +4,7 @@ import _m0 from "protobufjs/minimal"; import { InstrumentationScope, KeyValue } from "../../common/v1/common"; import { Resource } from "../../resource/v1/resource"; -export const protobufPackage = "opentelemetry.proto.metrics.v1"; +const protobufPackage = "opentelemetry.proto.metrics.v1"; /** * AggregationTemporality defines how a metric aggregator reports aggregated @@ -82,7 +82,7 @@ export enum AggregationTemporality { UNRECOGNIZED = -1, } -export function aggregationTemporalityFromJSON(object: any): AggregationTemporality { +function aggregationTemporalityFromJSON(object: any): AggregationTemporality { switch (object) { case 0: case "AGGREGATION_TEMPORALITY_UNSPECIFIED": @@ -100,7 +100,7 @@ export function aggregationTemporalityFromJSON(object: any): AggregationTemporal } } -export function aggregationTemporalityToJSON(object: AggregationTemporality): string { +function aggregationTemporalityToJSON(object: AggregationTemporality): string { switch (object) { case AggregationTemporality.UNSPECIFIED: return "AGGREGATION_TEMPORALITY_UNSPECIFIED"; @@ -122,7 +122,7 @@ export function aggregationTemporalityToJSON(object: AggregationTemporality): st * * (point.flags & DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK) == DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK */ -export enum DataPointFlags { +enum DataPointFlags { /** * DO_NOT_USE - The zero value for the enum. Should not be used for comparisons. * Instead use bitwise "and" with the appropriate mask as shown above. @@ -137,7 +137,7 @@ export enum DataPointFlags { UNRECOGNIZED = -1, } -export function dataPointFlagsFromJSON(object: any): DataPointFlags { +function dataPointFlagsFromJSON(object: any): DataPointFlags { switch (object) { case 0: case "DATA_POINT_FLAGS_DO_NOT_USE": @@ -152,7 +152,7 @@ export function dataPointFlagsFromJSON(object: any): DataPointFlags { } } -export function dataPointFlagsToJSON(object: DataPointFlags): string { +function dataPointFlagsToJSON(object: DataPointFlags): string { switch (object) { case DataPointFlags.DO_NOT_USE: return "DATA_POINT_FLAGS_DO_NOT_USE"; @@ -176,7 +176,7 @@ export function dataPointFlagsToJSON(object: DataPointFlags): string { * When new fields are added into this message, the OTLP request MUST be updated * as well. */ -export interface MetricsData { +interface MetricsData { /** * An array of ResourceMetrics. * For data coming from a single resource this array will typically contain @@ -649,7 +649,7 @@ export interface ExponentialHistogramDataPoint { * Buckets are a set of bucket counts, encoded in a contiguous array * of counts. */ -export interface ExponentialHistogramDataPoint_Buckets { +interface ExponentialHistogramDataPoint_Buckets { /** * Offset is the bucket index of the first entry in the bucket_counts array. * @@ -732,7 +732,7 @@ export interface SummaryDataPoint { * See the following issue for more context: * https://github.com/open-telemetry/opentelemetry-proto/issues/125 */ -export interface SummaryDataPoint_ValueAtQuantile { +interface SummaryDataPoint_ValueAtQuantile { /** * The quantile of a distribution. Must be in the interval * [0.0, 1.0]. @@ -752,7 +752,7 @@ export interface SummaryDataPoint_ValueAtQuantile { * was recorded, for example the span and trace ID of the active span when the * exemplar was recorded. */ -export interface Exemplar { +interface Exemplar { /** * The set of key/value pairs that were filtered out by the aggregator, but * recorded alongside the original measurement. Only key/value pairs that were @@ -786,7 +786,7 @@ function createBaseMetricsData(): MetricsData { return { resourceMetrics: [] }; } -export const MetricsData = { +const MetricsData = { encode(message: MetricsData, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { for (const v of message.resourceMetrics) { ResourceMetrics.encode(v!, writer.uint32(10).fork()).ldelim(); @@ -2387,7 +2387,7 @@ function createBaseExponentialHistogramDataPoint_Buckets(): ExponentialHistogram return { offset: 0, bucketCounts: [] }; } -export const ExponentialHistogramDataPoint_Buckets = { +const ExponentialHistogramDataPoint_Buckets = { encode( message: ExponentialHistogramDataPoint_Buckets, writer: _m0.Writer = _m0.Writer.create() @@ -2670,7 +2670,7 @@ function createBaseSummaryDataPoint_ValueAtQuantile(): SummaryDataPoint_ValueAtQ return { quantile: 0, value: 0 }; } -export const SummaryDataPoint_ValueAtQuantile = { +const SummaryDataPoint_ValueAtQuantile = { encode( message: SummaryDataPoint_ValueAtQuantile, writer: _m0.Writer = _m0.Writer.create() @@ -2758,7 +2758,7 @@ function createBaseExemplar(): Exemplar { }; } -export const Exemplar = { +const Exemplar = { encode(message: Exemplar, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { for (const v of message.filteredAttributes) { KeyValue.encode(v!, writer.uint32(58).fork()).ldelim(); @@ -2930,7 +2930,7 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin +type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> @@ -2941,7 +2941,7 @@ export type DeepPartial = T extends Builtin : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin +type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/resource/v1/resource.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/resource/v1/resource.ts index 60ac2239951..1a01c0eaf85 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/resource/v1/resource.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/resource/v1/resource.ts @@ -2,7 +2,7 @@ import _m0 from "protobufjs/minimal"; import { KeyValue } from "../../common/v1/common"; -export const protobufPackage = "opentelemetry.proto.resource.v1"; +const protobufPackage = "opentelemetry.proto.resource.v1"; /** Resource information. */ export interface Resource { @@ -99,7 +99,7 @@ export const Resource = { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin +type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> @@ -110,7 +110,7 @@ export type DeepPartial = T extends Builtin : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin +type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/trace/v1/trace.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/trace/v1/trace.ts index b90afa3c2c9..e7e09fc9d36 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/trace/v1/trace.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/trace/v1/trace.ts @@ -4,7 +4,7 @@ import _m0 from "protobufjs/minimal"; import { InstrumentationScope, KeyValue } from "../../common/v1/common"; import { Resource } from "../../resource/v1/resource"; -export const protobufPackage = "opentelemetry.proto.trace.v1"; +const protobufPackage = "opentelemetry.proto.trace.v1"; /** * SpanFlags represents constants used to interpret the @@ -40,7 +40,7 @@ export enum SpanFlags { UNRECOGNIZED = -1, } -export function spanFlagsFromJSON(object: any): SpanFlags { +function spanFlagsFromJSON(object: any): SpanFlags { switch (object) { case 0: case "SPAN_FLAGS_DO_NOT_USE": @@ -61,7 +61,7 @@ export function spanFlagsFromJSON(object: any): SpanFlags { } } -export function spanFlagsToJSON(object: SpanFlags): string { +function spanFlagsToJSON(object: SpanFlags): string { switch (object) { case SpanFlags.DO_NOT_USE: return "SPAN_FLAGS_DO_NOT_USE"; @@ -89,7 +89,7 @@ export function spanFlagsToJSON(object: SpanFlags): string { * When new fields are added into this message, the OTLP request MUST be updated * as well. */ -export interface TracesData { +interface TracesData { /** * An array of ResourceSpans. * For data coming from a single resource this array will typically contain @@ -318,7 +318,7 @@ export enum Span_SpanKind { UNRECOGNIZED = -1, } -export function span_SpanKindFromJSON(object: any): Span_SpanKind { +function span_SpanKindFromJSON(object: any): Span_SpanKind { switch (object) { case 0: case "SPAN_KIND_UNSPECIFIED": @@ -345,7 +345,7 @@ export function span_SpanKindFromJSON(object: any): Span_SpanKind { } } -export function span_SpanKindToJSON(object: Span_SpanKind): string { +function span_SpanKindToJSON(object: Span_SpanKind): string { switch (object) { case Span_SpanKind.UNSPECIFIED: return "SPAN_KIND_UNSPECIFIED"; @@ -467,7 +467,7 @@ export enum Status_StatusCode { UNRECOGNIZED = -1, } -export function status_StatusCodeFromJSON(object: any): Status_StatusCode { +function status_StatusCodeFromJSON(object: any): Status_StatusCode { switch (object) { case 0: case "STATUS_CODE_UNSET": @@ -485,7 +485,7 @@ export function status_StatusCodeFromJSON(object: any): Status_StatusCode { } } -export function status_StatusCodeToJSON(object: Status_StatusCode): string { +function status_StatusCodeToJSON(object: Status_StatusCode): string { switch (object) { case Status_StatusCode.UNSET: return "STATUS_CODE_UNSET"; @@ -503,7 +503,7 @@ function createBaseTracesData(): TracesData { return { resourceSpans: [] }; } -export const TracesData = { +const TracesData = { encode(message: TracesData, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { for (const v of message.resourceSpans) { ResourceSpans.encode(v!, writer.uint32(10).fork()).ldelim(); @@ -1441,7 +1441,7 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin +type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> @@ -1452,7 +1452,7 @@ export type DeepPartial = T extends Builtin : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin +type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; diff --git a/internal-packages/otlp-importer/tsup.config.ts b/internal-packages/otlp-importer/tsup.config.ts deleted file mode 100644 index d4d70580539..00000000000 --- a/internal-packages/otlp-importer/tsup.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - name: "main", - config: "tsconfig.build.json", - entry: ["./src/index.ts"], - outDir: "./dist", - platform: "node", - format: ["cjs", "esm"], - legacyOutput: false, - sourcemap: true, - clean: true, - bundle: true, - splitting: false, - dts: true, - treeshake: { - preset: "recommended", - }, -}); diff --git a/internal-packages/run-engine/src/engine/controlPlaneResolver.ts b/internal-packages/run-engine/src/engine/controlPlaneResolver.ts index 114095cc676..a0a6b451442 100644 --- a/internal-packages/run-engine/src/engine/controlPlaneResolver.ts +++ b/internal-packages/run-engine/src/engine/controlPlaneResolver.ts @@ -63,7 +63,7 @@ export type ResolvedWorkerTask = { }; /** The `select` that yields a `ResolvedWorkerTask`. */ -export const resolvedWorkerTaskSelect = { +const resolvedWorkerTaskSelect = { id: true, slug: true, machineConfig: true, @@ -81,7 +81,7 @@ export type ResolvedTaskQueue = { }; /** The `select` that yields a `ResolvedTaskQueue`. */ -export const resolvedTaskQueueSelect = { +const resolvedTaskQueueSelect = { id: true, name: true, } satisfies Prisma.TaskQueueSelect; @@ -99,7 +99,7 @@ export type ResolvedWorkerDeployment = { }; /** The `select` that yields a `ResolvedWorkerDeployment`. */ -export const resolvedWorkerDeploymentSelect = { +const resolvedWorkerDeploymentSelect = { id: true, friendlyId: true, imageReference: true, @@ -144,7 +144,7 @@ type WorkerVersionWheres = { }; /** Build the nested-include `where`s for a dispatch filter (undefined = fetch the whole set). */ -export function workerVersionWheres(filter: WorkerVersionDispatchFilter): WorkerVersionWheres { +function workerVersionWheres(filter: WorkerVersionDispatchFilter): WorkerVersionWheres { return { taskWhere: filter.taskIdentifier ? { slug: filter.taskIdentifier } : undefined, queueWhere: filter.queue diff --git a/internal-packages/run-engine/src/engine/systems/debounceSystem.ts b/internal-packages/run-engine/src/engine/systems/debounceSystem.ts index bbe5bbbd65a..5cf5ba35b59 100644 --- a/internal-packages/run-engine/src/engine/systems/debounceSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/debounceSystem.ts @@ -98,12 +98,6 @@ const MAX_CLAIM_RETRIES = 10; // Delay between retries when waiting for pending claim const CLAIM_RETRY_DELAY_MS = 50; -export type DebounceData = { - key: string; - delay: string; - createdAt: Date; -}; - /** * DebounceSystem handles debouncing of task triggers. * diff --git a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts index c7a331fe9de..e999d35676d 100644 --- a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts @@ -2126,7 +2126,7 @@ export class RunAttemptSystem { } } -export function safeParseGitMeta(git: unknown): GitMeta | undefined { +function safeParseGitMeta(git: unknown): GitMeta | undefined { const parsed = GitMeta.safeParse(git); if (parsed.success) { return parsed.data; diff --git a/internal-packages/run-engine/src/engine/tests/helpers/executionStateMachine.ts b/internal-packages/run-engine/src/engine/tests/helpers/executionStateMachine.ts deleted file mode 100644 index 6e18e254cab..00000000000 --- a/internal-packages/run-engine/src/engine/tests/helpers/executionStateMachine.ts +++ /dev/null @@ -1,257 +0,0 @@ -import type { TaskRunExecutionStatus } from "@trigger.dev/database"; - -/** - * Defines valid execution status transitions for the Run Engine 2.0. - * This is a model of the state machine that governs run execution. - */ -export const EXECUTION_STATUS_TRANSITIONS: Record< - TaskRunExecutionStatus, - TaskRunExecutionStatus[] -> = { - RUN_CREATED: ["QUEUED", "DELAYED"], - DELAYED: ["QUEUED"], - QUEUED: ["PENDING_EXECUTING", "QUEUED_EXECUTING"], - QUEUED_EXECUTING: ["PENDING_EXECUTING", "QUEUED"], - PENDING_EXECUTING: ["EXECUTING", "PENDING_CANCEL", "FINISHED", "QUEUED"], - EXECUTING: ["EXECUTING_WITH_WAITPOINTS", "FINISHED", "PENDING_CANCEL", "QUEUED"], - EXECUTING_WITH_WAITPOINTS: ["EXECUTING", "SUSPENDED", "FINISHED", "PENDING_CANCEL"], - SUSPENDED: ["QUEUED", "PENDING_CANCEL", "FINISHED"], - PENDING_CANCEL: ["FINISHED"], - FINISHED: ["QUEUED"], // Retry case -}; - -/** - * Validates if a transition from one status to another is valid. - */ -export function isValidTransition( - from: TaskRunExecutionStatus, - to: TaskRunExecutionStatus -): boolean { - return EXECUTION_STATUS_TRANSITIONS[from]?.includes(to) ?? false; -} - -/** - * Configuration for a snapshot in a test scenario. - */ -export interface SnapshotConfig { - /** The execution status for this snapshot */ - status: TaskRunExecutionStatus; - /** Number of waitpoints completed at this snapshot (cumulative) */ - completedWaitpointCount: number; - /** Whether this snapshot has a checkpoint */ - hasCheckpoint?: boolean; - /** Description for the snapshot */ - description?: string; -} - -/** - * A test scenario for getSnapshotsSince testing. - */ -export interface SnapshotTestScenario { - /** Unique name for the scenario */ - name: string; - /** Description of what this scenario tests */ - description: string; - /** Total number of waitpoints to create */ - totalWaitpoints: number; - /** Size of each waitpoint's output in KB */ - outputSizeKB: number; - /** Configuration for each snapshot to create */ - snapshots: SnapshotConfig[]; - /** Which snapshot index to query "since" (0-based) */ - queryFromIndex: number; - /** Expected number of waitpoints on the latest snapshot returned */ - expectedWaitpointsOnLatest: number; -} - -/** - * Generates test scenarios for comprehensive getSnapshotsSince testing. - * These scenarios cover various edge cases and stress tests. - */ -export function generateTestScenarios(): SnapshotTestScenario[] { - return [ - { - name: "simple_no_waitpoints", - description: "Basic run without any waitpoints", - totalWaitpoints: 0, - outputSizeKB: 0, - snapshots: [ - { status: "RUN_CREATED", completedWaitpointCount: 0 }, - { status: "QUEUED", completedWaitpointCount: 0 }, - { status: "PENDING_EXECUTING", completedWaitpointCount: 0 }, - { status: "EXECUTING", completedWaitpointCount: 0 }, - { status: "FINISHED", completedWaitpointCount: 0 }, - ], - queryFromIndex: 0, - expectedWaitpointsOnLatest: 0, - }, - { - name: "single_small_waitpoint", - description: "Single waitpoint with small output", - totalWaitpoints: 1, - outputSizeKB: 1, - snapshots: [ - { status: "RUN_CREATED", completedWaitpointCount: 0 }, - { status: "QUEUED", completedWaitpointCount: 0 }, - { status: "EXECUTING", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 0 }, - { status: "EXECUTING", completedWaitpointCount: 1 }, - ], - queryFromIndex: 2, - expectedWaitpointsOnLatest: 1, - }, - { - name: "batch_100_medium", - description: "Medium batch with 100 waitpoints and medium outputs", - totalWaitpoints: 100, - outputSizeKB: 10, - snapshots: [ - { status: "RUN_CREATED", completedWaitpointCount: 0 }, - { status: "QUEUED", completedWaitpointCount: 0 }, - { status: "EXECUTING", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 50 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 100 }, - { status: "SUSPENDED", completedWaitpointCount: 100, hasCheckpoint: true }, - { status: "QUEUED", completedWaitpointCount: 100 }, - { status: "EXECUTING", completedWaitpointCount: 100 }, - { status: "FINISHED", completedWaitpointCount: 100 }, - ], - queryFromIndex: 3, - expectedWaitpointsOnLatest: 100, - }, - { - name: "batch_236_large_zombie_scenario", - description: - "Matches the zombie run scenario: 24 snapshots, 236 waitpoints, 100KB outputs each", - totalWaitpoints: 236, - outputSizeKB: 100, - snapshots: [ - { status: "RUN_CREATED", completedWaitpointCount: 0 }, - { status: "QUEUED", completedWaitpointCount: 0 }, - { status: "PENDING_EXECUTING", completedWaitpointCount: 0 }, - { status: "EXECUTING", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 50 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 100 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 150 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 200 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 236 }, - { status: "SUSPENDED", completedWaitpointCount: 236, hasCheckpoint: true }, - { status: "QUEUED", completedWaitpointCount: 236 }, - { status: "PENDING_EXECUTING", completedWaitpointCount: 236 }, - { status: "EXECUTING", completedWaitpointCount: 236 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 236 }, - { status: "SUSPENDED", completedWaitpointCount: 236, hasCheckpoint: true }, - { status: "QUEUED", completedWaitpointCount: 236 }, - { status: "PENDING_EXECUTING", completedWaitpointCount: 236 }, - { status: "EXECUTING", completedWaitpointCount: 236 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 236 }, - { status: "SUSPENDED", completedWaitpointCount: 236, hasCheckpoint: true }, - { status: "QUEUED", completedWaitpointCount: 236 }, - { status: "PENDING_EXECUTING", completedWaitpointCount: 236 }, - { status: "EXECUTING", completedWaitpointCount: 236 }, - ], - queryFromIndex: 6, - expectedWaitpointsOnLatest: 236, - }, - { - name: "batch_500_large", - description: "Large batch requiring chunked fetching", - totalWaitpoints: 500, - outputSizeKB: 50, - snapshots: [ - { status: "RUN_CREATED", completedWaitpointCount: 0 }, - { status: "QUEUED", completedWaitpointCount: 0 }, - { status: "EXECUTING", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 100 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 250 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 400 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 500 }, - { status: "SUSPENDED", completedWaitpointCount: 500, hasCheckpoint: true }, - { status: "QUEUED", completedWaitpointCount: 500 }, - { status: "EXECUTING", completedWaitpointCount: 500 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 500 }, - { status: "SUSPENDED", completedWaitpointCount: 500, hasCheckpoint: true }, - { status: "QUEUED", completedWaitpointCount: 500 }, - { status: "EXECUTING", completedWaitpointCount: 500 }, - ], - queryFromIndex: 5, - expectedWaitpointsOnLatest: 500, - }, - { - name: "system_failure_finished", - description: "Latest snapshot is FINISHED status with completed waitpoints", - totalWaitpoints: 100, - outputSizeKB: 50, - snapshots: [ - { status: "RUN_CREATED", completedWaitpointCount: 0 }, - { status: "QUEUED", completedWaitpointCount: 0 }, - { status: "EXECUTING", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 50 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 100 }, - { status: "EXECUTING", completedWaitpointCount: 100 }, - { status: "FINISHED", completedWaitpointCount: 100 }, - ], - queryFromIndex: 3, - expectedWaitpointsOnLatest: 100, - }, - { - name: "query_from_latest", - description: "Querying from the latest snapshot should return empty array", - totalWaitpoints: 10, - outputSizeKB: 10, - snapshots: [ - { status: "RUN_CREATED", completedWaitpointCount: 0 }, - { status: "QUEUED", completedWaitpointCount: 0 }, - { status: "EXECUTING", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 0 }, - { status: "EXECUTING", completedWaitpointCount: 10 }, - ], - queryFromIndex: 4, // The last snapshot - expectedWaitpointsOnLatest: 0, // No snapshots returned, so no waitpoints - }, - { - name: "requeue_loop", - description: "Multiple QUEUED->PENDING_EXECUTING cycles with waitpoints", - totalWaitpoints: 236, - outputSizeKB: 100, - snapshots: [ - { status: "RUN_CREATED", completedWaitpointCount: 0 }, - { status: "QUEUED", completedWaitpointCount: 0 }, - { status: "PENDING_EXECUTING", completedWaitpointCount: 0 }, - { status: "EXECUTING", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 0 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 236 }, - { status: "SUSPENDED", completedWaitpointCount: 236, hasCheckpoint: true }, - { status: "QUEUED", completedWaitpointCount: 236 }, - { status: "PENDING_EXECUTING", completedWaitpointCount: 236 }, - { status: "QUEUED", completedWaitpointCount: 236 }, // Requeued - { status: "PENDING_EXECUTING", completedWaitpointCount: 236 }, - { status: "QUEUED", completedWaitpointCount: 236 }, // Requeued again - { status: "PENDING_EXECUTING", completedWaitpointCount: 236 }, - { status: "EXECUTING", completedWaitpointCount: 236 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 236 }, - { status: "SUSPENDED", completedWaitpointCount: 236, hasCheckpoint: true }, - { status: "QUEUED", completedWaitpointCount: 236 }, - { status: "PENDING_EXECUTING", completedWaitpointCount: 236 }, - { status: "QUEUED", completedWaitpointCount: 236 }, // Requeued - { status: "PENDING_EXECUTING", completedWaitpointCount: 236 }, - { status: "QUEUED", completedWaitpointCount: 236 }, // Requeued again - { status: "PENDING_EXECUTING", completedWaitpointCount: 236 }, - { status: "EXECUTING", completedWaitpointCount: 236 }, - { status: "EXECUTING_WITH_WAITPOINTS", completedWaitpointCount: 236 }, - { status: "SUSPENDED", completedWaitpointCount: 236, hasCheckpoint: true }, - { status: "QUEUED", completedWaitpointCount: 236 }, - { status: "PENDING_EXECUTING", completedWaitpointCount: 236 }, - { status: "QUEUED", completedWaitpointCount: 236 }, // Requeued - { status: "PENDING_EXECUTING", completedWaitpointCount: 236 }, - { status: "EXECUTING", completedWaitpointCount: 236 }, - ], - queryFromIndex: 7, - expectedWaitpointsOnLatest: 236, - }, - ]; -} diff --git a/internal-packages/run-engine/src/engine/tests/helpers/snapshotTestHelpers.ts b/internal-packages/run-engine/src/engine/tests/helpers/snapshotTestHelpers.ts index 099d6b5bb39..1f71b9b14e0 100644 --- a/internal-packages/run-engine/src/engine/tests/helpers/snapshotTestHelpers.ts +++ b/internal-packages/run-engine/src/engine/tests/helpers/snapshotTestHelpers.ts @@ -12,7 +12,7 @@ import type { AuthenticatedEnvironment } from "../setup.js"; * Generates a large output string of the specified size in KB. * The output is a valid JSON string to simulate realistic waitpoint output. */ -export function generateLargeOutput(sizeKB: number): string { +function generateLargeOutput(sizeKB: number): string { if (sizeKB <= 0) return JSON.stringify({ data: "" }); // Create a string that's approximately the target size @@ -29,7 +29,7 @@ export function generateLargeOutput(sizeKB: number): string { /** * Creates waitpoints with specified output sizes for testing. */ -export async function createWaitpointsWithOutput( +async function createWaitpointsWithOutput( prisma: PrismaClient, count: number, outputSizeKB: number, @@ -172,7 +172,7 @@ function getRunStatusFromExecutionStatus( /** * Creates a checkpoint for testing suspended snapshots. */ -export async function createTestCheckpoint( +async function createTestCheckpoint( prisma: PrismaClient, { runId, diff --git a/internal-packages/run-engine/src/engine/tests/utils/engineTest.ts b/internal-packages/run-engine/src/engine/tests/utils/engineTest.ts deleted file mode 100644 index fee29415b91..00000000000 --- a/internal-packages/run-engine/src/engine/tests/utils/engineTest.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { type TestContext, type TestAPI, test } from "vitest"; -import { - type StartedNetwork, - type StartedPostgreSqlContainer, - type StartedRedisContainer, - logCleanup, - network, - postgresContainer, - prisma, - redisContainer, - redisOptions, - type PostgresAndRedisContext, -} from "@internal/testcontainers"; -import { RunEngine } from "../../index.js"; -import type { PrismaClient } from "@trigger.dev/database"; -import type { RedisOptions } from "@internal/redis"; -import { trace } from "@internal/tracing"; -import type { RunEngineOptions } from "../../types.js"; - -type Use = (value: T) => Promise; - -type EngineOptions = { - worker?: { - workers?: number; - tasksPerWorker?: number; - pollIntervalMs?: number; - }; - queue?: { - processWorkerQueueDebounceMs?: number; - masterQueueConsumersDisabled?: boolean; - }; - machines?: { - defaultMachine?: RunEngineOptions["machines"]["defaultMachine"]; - machines?: RunEngineOptions["machines"]["machines"]; - baseCostInCents?: number; - }; -}; - -const engineOptions = async ({}: TestContext, use: Use) => { - const options: EngineOptions = { - worker: { - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - processWorkerQueueDebounceMs: 50, - masterQueueConsumersDisabled: true, - }, - machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, - }, - baseCostInCents: 0.0001, - }, - }; - - await use(options); -}; - -const engine = async ( - { - engineOptions, - task, - redisOptions, - prisma, - }: { - engineOptions: EngineOptions; - redisOptions: RedisOptions; - prisma: PrismaClient; - } & TestContext, - use: Use -) => { - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: engineOptions.worker?.workers ?? 1, - tasksPerWorker: engineOptions.worker?.tasksPerWorker ?? 10, - pollIntervalMs: engineOptions.worker?.pollIntervalMs ?? 100, - }, - queue: { - redis: redisOptions, - processWorkerQueueDebounceMs: engineOptions.queue?.processWorkerQueueDebounceMs ?? 50, - masterQueueConsumersDisabled: engineOptions.queue?.masterQueueConsumersDisabled ?? true, - }, - runLock: { - redis: redisOptions, - }, - machines: { - defaultMachine: engineOptions.machines?.defaultMachine ?? ("small-1x" as const), - machines: engineOptions.machines?.machines ?? {}, - baseCostInCents: engineOptions.machines?.baseCostInCents ?? 0.0001, - }, - tracer: trace.getTracer("test", "0.0.0"), - }); - - const testName = task.name; - - try { - await use(engine); - } finally { - await logCleanup("engine", engine.quit(), { testName }); - } -}; - -export type EngineContext = PostgresAndRedisContext & { - engineOptions: EngineOptions; - engine: RunEngine; -}; - -export const engineTest: TestAPI<{ - redisOptions: RedisOptions; - prisma: PrismaClient; - engineOptions: EngineOptions; - engine: RunEngine; - network: StartedNetwork; - postgresContainer: StartedPostgreSqlContainer; - redisContainer: StartedRedisContainer; -}> = test.extend({ - network, - postgresContainer, - prisma, - redisContainer, - redisOptions, - engineOptions, - engine, -}); diff --git a/internal-packages/run-engine/src/engine/ttlWorkerCatalog.ts b/internal-packages/run-engine/src/engine/ttlWorkerCatalog.ts index e571d809d98..7fccee55aee 100644 --- a/internal-packages/run-engine/src/engine/ttlWorkerCatalog.ts +++ b/internal-packages/run-engine/src/engine/ttlWorkerCatalog.ts @@ -22,5 +22,3 @@ export function createTtlWorkerCatalog(options?: TtlWorkerCatalogOptions) { }, }; } - -export const ttlWorkerCatalog = createTtlWorkerCatalog(); diff --git a/internal-packages/run-engine/src/engine/types.ts b/internal-packages/run-engine/src/engine/types.ts index 547852803b2..9b7a3b1b8fd 100644 --- a/internal-packages/run-engine/src/engine/types.ts +++ b/internal-packages/run-engine/src/engine/types.ts @@ -30,7 +30,7 @@ import type { PendingVersionRunIdLookup } from "./services/pendingVersionLookup. * Re-declared here because @internal/run-engine must not depend on the webapp. * Keep field names identical so the injected value is assignable. */ -export type CrossSeamGuardDecision = { +type CrossSeamGuardDecision = { store: "new" | "legacy"; residency: "NEW" | "LEGACY"; routeKind: string; diff --git a/internal-packages/run-engine/src/run-queue/constants.ts b/internal-packages/run-engine/src/run-queue/constants.ts index 22e1928fb77..a5f4ad36baa 100644 --- a/internal-packages/run-engine/src/run-queue/constants.ts +++ b/internal-packages/run-engine/src/run-queue/constants.ts @@ -1,4 +1 @@ export const RUN_QUEUE_RESUME_PRIORITY_TIMESTAMP_OFFSET = 31_556_952 * 1000; // 1 year -export const RUN_QUEUE_RETRY_PRIORITY_TIMESTAMP_OFFSET = 15_778_476 * 1000; // 6 months -export const RUN_QUEUE_DELAYED_REQUEUE_THRESHOLD_IN_MS = 500; -export const RUN_QUEUE_SCHEDULED_REQUEUE_AVAILABLE_AT_THRESHOLD_IN_MS = 500; diff --git a/internal-packages/run-engine/src/run-queue/errors.ts b/internal-packages/run-engine/src/run-queue/errors.ts deleted file mode 100644 index eecebdab541..00000000000 --- a/internal-packages/run-engine/src/run-queue/errors.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class MessageNotFoundError extends Error { - constructor(messageId: string) { - super(`Message not found: ${messageId}`); - } -} diff --git a/internal-packages/run-engine/src/run-queue/fairQueueSelectionStrategy.ts b/internal-packages/run-engine/src/run-queue/fairQueueSelectionStrategy.ts index a9f21d2340d..b6839883739 100644 --- a/internal-packages/run-engine/src/run-queue/fairQueueSelectionStrategy.ts +++ b/internal-packages/run-engine/src/run-queue/fairQueueSelectionStrategy.ts @@ -16,7 +16,7 @@ import type { RunQueueSelectionStrategy, } from "./types.js"; -export type FairQueueSelectionStrategyBiases = { +type FairQueueSelectionStrategyBiases = { /** * How much to bias towards environments with higher concurrency limits * 0 = no bias, 1 = full bias based on limit differences @@ -626,12 +626,3 @@ export class FairQueueSelectionStrategy implements RunQueueSelectionStrategy { }; } } - -export class NoopFairDequeuingStrategy implements RunQueueSelectionStrategy { - async distributeFairQueuesFromParentQueue( - parentQueue: string, - consumerId: string - ): Promise> { - return []; - } -} diff --git a/internal-packages/run-engine/src/run-queue/index.ts b/internal-packages/run-engine/src/run-queue/index.ts index b5a7eba25af..57cfe518f37 100644 --- a/internal-packages/run-engine/src/run-queue/index.ts +++ b/internal-packages/run-engine/src/run-queue/index.ts @@ -216,7 +216,7 @@ export type RunQueueOptions = { }; }; -export interface ConcurrencySweeperCallback { +interface ConcurrencySweeperCallback { (runIds: string[]): Promise>; } diff --git a/internal-packages/run-store/package.json b/internal-packages/run-store/package.json index 481dcae4b9a..110c3b49058 100644 --- a/internal-packages/run-store/package.json +++ b/internal-packages/run-store/package.json @@ -14,11 +14,11 @@ } }, "dependencies": { - "@internal/run-ops-database": "workspace:*", "@trigger.dev/core": "workspace:*", "@trigger.dev/database": "workspace:*" }, "devDependencies": { + "@internal/run-ops-database": "workspace:*", "@internal/testcontainers": "workspace:*", "rimraf": "6.0.1" }, diff --git a/internal-packages/schedule-engine/README.md b/internal-packages/schedule-engine/README.md index 28be432ee8a..70498dd1fdb 100644 --- a/internal-packages/schedule-engine/README.md +++ b/internal-packages/schedule-engine/README.md @@ -62,13 +62,9 @@ const distributedTime = calculateDistributedExecutionTime(exactTime, 30); // 30- High-performance CRON schedule calculation with optimization for old timestamps: ```typescript -import { - calculateNextScheduledTimestampFromNow, - nextScheduledTimestamps, -} from "@internal/schedule-engine"; +import { calculateNextNominalTimestamp } from "@internal/schedule-engine"; -const nextRun = calculateNextScheduledTimestampFromNow("0 */5 * * *", "UTC"); -const upcoming = nextScheduledTimestamps("0 */5 * * *", "UTC", nextRun, 5); +const nextRun = calculateNextNominalTimestamp("0 */5 * * *", "UTC", new Date()); ``` ## Integration with Webapp diff --git a/internal-packages/schedule-engine/package.json b/internal-packages/schedule-engine/package.json index 2545428c294..9f77fa6362e 100644 --- a/internal-packages/schedule-engine/package.json +++ b/internal-packages/schedule-engine/package.json @@ -20,7 +20,6 @@ "@trigger.dev/core": "workspace:*", "@trigger.dev/database": "workspace:*", "cron-parser": "^4.9.0", - "cronstrue": "^2.50.0", "zod": "3.25.76" }, "devDependencies": { diff --git a/internal-packages/schedule-engine/src/engine/scheduleCalculation.ts b/internal-packages/schedule-engine/src/engine/scheduleCalculation.ts index 074aae16042..c8e6c4c9697 100644 --- a/internal-packages/schedule-engine/src/engine/scheduleCalculation.ts +++ b/internal-packages/schedule-engine/src/engine/scheduleCalculation.ts @@ -5,10 +5,6 @@ import { type NormalizedScheduleWindow, } from "./scheduleTiming.js"; -export function calculateNextScheduledTimestampFromNow(schedule: string, timezone: string | null) { - return calculateNextScheduledTimestamp(schedule, timezone, new Date()); -} - export function calculateNextNominalTimestamp( schedule: string, timezone: string | null, @@ -17,21 +13,6 @@ export function calculateNextNominalTimestamp( return calculateNextStep(schedule, timezone, nominalTimestamp); } -export function calculateNextScheduledTimestamp( - schedule: string, - timezone: string | null, - lastScheduledTimestamp: Date = new Date() -) { - const nextStep = calculateNextStep(schedule, timezone, lastScheduledTimestamp); - - if (nextStep.getTime() < Date.now()) { - // If the next step is in the past, we just need to calculate the next step from now - return calculateNextStep(schedule, timezone, new Date()); - } - - return nextStep; -} - function calculateNextStep(schedule: string, timezone: string | null, currentDate: Date) { return parseExpression(schedule, { currentDate, diff --git a/internal-packages/schedule-engine/src/engine/types.ts b/internal-packages/schedule-engine/src/engine/types.ts index 58e089dab03..4cb72fd2f6e 100644 --- a/internal-packages/schedule-engine/src/engine/types.ts +++ b/internal-packages/schedule-engine/src/engine/types.ts @@ -3,7 +3,7 @@ import type { Meter, Tracer } from "@internal/tracing"; import type { Prisma, PrismaClient } from "@trigger.dev/database"; import type { RedisOptions } from "@internal/redis"; -export type SchedulingEnvironment = Prisma.RuntimeEnvironmentGetPayload<{ +type SchedulingEnvironment = Prisma.RuntimeEnvironmentGetPayload<{ include: { project: true; organization: true; orgMember: true }; }>; @@ -66,19 +66,6 @@ export interface ScheduleEngineOptions { onRegisterScheduleInstance?: (instanceId: string) => Promise; } -export interface UpsertScheduleParams { - projectId: string; - schedule: { - friendlyId?: string; - taskIdentifier: string; - deduplicationKey?: string; - cron: string; - timezone?: string; - externalId?: string; - environments: string[]; - }; -} - export interface TriggerScheduleParams { instanceId: string; finalAttempt: boolean; diff --git a/internal-packages/sdk-compat-tests/package.json b/internal-packages/sdk-compat-tests/package.json index 568f3ad7796..d2106672729 100644 --- a/internal-packages/sdk-compat-tests/package.json +++ b/internal-packages/sdk-compat-tests/package.json @@ -8,10 +8,8 @@ "test:watch": "vitest", "typecheck": "tsc --noEmit" }, - "dependencies": { - "@trigger.dev/sdk": "workspace:*" - }, "devDependencies": { + "@trigger.dev/sdk": "workspace:*", "esbuild": "^0.24.0", "execa": "^9.3.0", "typescript": "catalog:", diff --git a/internal-packages/sso/package.json b/internal-packages/sso/package.json index 187338f18b4..b224d6e5e75 100644 --- a/internal-packages/sso/package.json +++ b/internal-packages/sso/package.json @@ -5,7 +5,6 @@ "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { - "@trigger.dev/core": "workspace:*", "@trigger.dev/plugins": "workspace:*", "neverthrow": "^8.2.0" }, diff --git a/internal-packages/testcontainers/package.json b/internal-packages/testcontainers/package.json index df0df141c66..c1e68946aa6 100644 --- a/internal-packages/testcontainers/package.json +++ b/internal-packages/testcontainers/package.json @@ -14,7 +14,6 @@ }, "dependencies": { "@clickhouse/client": "^1.11.1", - "@opentelemetry/api": "^1.9.1", "@trigger.dev/database": "workspace:*", "ioredis": "~5.6.0" }, diff --git a/internal-packages/testcontainers/src/docker.ts b/internal-packages/testcontainers/src/docker.ts index 45cacb98aab..ddcfa74c987 100644 --- a/internal-packages/testcontainers/src/docker.ts +++ b/internal-packages/testcontainers/src/docker.ts @@ -34,7 +34,7 @@ type DockerNetworkAttachment = DockerResource & { containers: string[]; }; -export async function getDockerNetworkAttachments(): Promise { +async function getDockerNetworkAttachments(): Promise { let attachments: DockerNetworkAttachment[] = []; let networks: DockerResource[] = []; @@ -88,7 +88,7 @@ type DockerContainerNetwork = DockerResource & { networks: string[]; }; -export async function getDockerContainerNetworks(): Promise { +async function getDockerContainerNetworks(): Promise { let results: DockerContainerNetwork[] = []; let containers: DockerResource[] = []; diff --git a/internal-packages/testcontainers/src/utils.ts b/internal-packages/testcontainers/src/utils.ts index fa5dd310cc6..e0dec64703f 100644 --- a/internal-packages/testcontainers/src/utils.ts +++ b/internal-packages/testcontainers/src/utils.ts @@ -12,7 +12,6 @@ import { GenericContainer, Wait } from "testcontainers"; import { x } from "tinyexec"; import type { TestContext } from "vitest"; import { ClickHouseContainer, runClickhouseMigrations } from "./clickhouse"; -import { MinIOContainer } from "./minio"; import { getContainerMetadata, getTaskMetadata, logCleanup, logSetup } from "./logs"; async function tryCatch(promise: Promise): Promise<[E, null] | [null, T]> { @@ -305,18 +304,6 @@ export async function createElectricContainer( }; } -export async function createMinIOContainer(network: StartedNetwork) { - const container = await withCiResourceLimits(new MinIOContainer()) - .withNetwork(network) - .withNetworkAliases("minio") - .start(); - - return { - container, - network, - }; -} - export function assertNonNullable(value: T): asserts value is NonNullable { // Plain throw — *not* `vitest.expect`. Two reasons: // 1. This module is imported by globalSetup files that run before any diff --git a/internal-packages/tsql/package.json b/internal-packages/tsql/package.json index 0cac36e7b27..43c17a2aec2 100644 --- a/internal-packages/tsql/package.json +++ b/internal-packages/tsql/package.json @@ -6,9 +6,7 @@ "types": "./src/index.ts", "type": "module", "dependencies": { - "@trigger.dev/core": "workspace:*", - "antlr4ts": "0.5.0-alpha.4", - "zod": "3.25.76" + "antlr4ts": "0.5.0-alpha.4" }, "scripts": { "typecheck": "tsc --noEmit", diff --git a/internal-packages/tsql/src/query/constants.ts b/internal-packages/tsql/src/query/constants.ts index f698d3cc7aa..09e6eef012c 100644 --- a/internal-packages/tsql/src/query/constants.ts +++ b/internal-packages/tsql/src/query/constants.ts @@ -12,27 +12,9 @@ export type ConstantDataType = | "uuid" | "unknown"; -export type ConstantSupportedPrimitive = number | string | boolean | Date | null; -export type ConstantSupportedData = - | ConstantSupportedPrimitive - | ConstantSupportedPrimitive[] - | [ConstantSupportedPrimitive, ...ConstantSupportedPrimitive[]]; - -export const KEYWORDS = ["true", "false", "null"] as const; +const KEYWORDS = ["true", "false", "null"] as const; export const RESERVED_KEYWORDS = [...KEYWORDS, "team_id"] as const; -export const DEFAULT_RETURNED_ROWS = 100; -export const MAX_SELECT_RETURNED_ROWS = 50000; -export const MAX_SELECT_RETENTION_LIMIT = 100000; -export const MAX_SELECT_HEATMAPS_LIMIT = 1000000; -export const MAX_SELECT_COHORT_CALCULATION_LIMIT = 1000000000; -export const MAX_BYTES_BEFORE_EXTERNAL_GROUP_BY = 22 * 1024 * 1024 * 1024; -export const CSV_EXPORT_LIMIT = 300000; -export const CSV_EXPORT_BREAKDOWN_LIMIT_INITIAL = 512; -export const CSV_EXPORT_BREAKDOWN_LIMIT_LOW = 64; -export const BREAKDOWN_VALUES_LIMIT = 25; -export const BREAKDOWN_VALUES_LIMIT_FOR_COUNTRIES = 300; - export enum LimitContext { QUERY = "query", QUERY_ASYNC = "query_async", @@ -50,22 +32,3 @@ export interface TSQLQuerySettings { date_time_input_format?: string; join_algorithm?: string; } - -// Settings applied on top of all TSQL queries -export interface TSQLGlobalSettings extends TSQLQuerySettings { - readonly?: number; - max_execution_time?: number; - max_memory_usage?: number; - max_threads?: number; - allow_experimental_object_type?: boolean; - format_csv_allow_double_quotes?: boolean; - max_ast_elements?: number; - max_expanded_ast_elements?: number; - max_bytes_before_external_group_by?: number; - allow_experimental_analyzer?: boolean; - transform_null_in?: boolean; - optimize_min_equality_disjunction_chain_length?: number; - allow_experimental_join_condition?: boolean; - preferred_block_size_bytes?: number; - use_hive_partitioning?: number; -} diff --git a/internal-packages/tsql/src/query/context.ts b/internal-packages/tsql/src/query/context.ts index 249d3faa282..54d7d84b643 100644 --- a/internal-packages/tsql/src/query/context.ts +++ b/internal-packages/tsql/src/query/context.ts @@ -5,7 +5,7 @@ import type { Database } from "./database"; import type { PropertySwapper } from "./property_types"; import type { TSQLTimings } from "./timings"; -export interface TSQLNotice { +interface TSQLNotice { start?: number; end?: number; message: string; @@ -23,13 +23,6 @@ export interface TSQLQueryModifiers { optimizeProjections?: boolean; } -export interface TSQLFieldAccess { - input: string[]; - type?: "run"; - field?: string; - sql: string; -} - export interface Team { id: number; project_id: number; diff --git a/internal-packages/tsql/src/query/database.ts b/internal-packages/tsql/src/query/database.ts index 1e4ed5d8b43..01b734887a3 100644 --- a/internal-packages/tsql/src/query/database.ts +++ b/internal-packages/tsql/src/query/database.ts @@ -28,50 +28,13 @@ export interface DatabaseSchemaTable { name: string; } -export interface DatabaseSchemaSystemTable extends DatabaseSchemaTable { +interface DatabaseSchemaSystemTable extends DatabaseSchemaTable { fields: Record; id: string; name: string; } -export interface DatabaseSchemaDataWarehouseTable extends DatabaseSchemaTable { - fields: Record; - id: string; - name: string; - format?: string; - url_pattern?: string; - schema?: DatabaseSchemaSchema; - source?: DatabaseSchemaSource; - row_count?: number; -} - -export interface DatabaseSchemaViewTable extends DatabaseSchemaTable { - fields: Record; - id: string; - name: string; - query: { query: string }; - row_count?: number; -} - -export interface DatabaseSchemaManagedViewTable extends DatabaseSchemaTable { - fields: Record; - id: string; - name: string; - kind: string; - source_id?: string; - query: { query: string }; -} - -export interface DatabaseSchemaEndpointTable extends DatabaseSchemaTable { - fields: Record; - id: string; - name: string; - query: { query: string }; - row_count?: number; - status?: string; -} - -export interface DatabaseSchemaField { +interface DatabaseSchemaField { name: string; tsql_value: string; type: DatabaseSerializedFieldType; @@ -82,24 +45,7 @@ export interface DatabaseSchemaField { id?: string; } -export interface DatabaseSchemaSchema { - id: string; - name: string; - should_sync: boolean; - incremental: boolean; - status: string; - last_synced_at: string; -} - -export interface DatabaseSchemaSource { - id: string; - status: string; - source_type: string; - prefix: string; - last_synced_at?: string | null; -} - -export enum DatabaseSerializedFieldType { +enum DatabaseSerializedFieldType { STRING = "string", INTEGER = "integer", FLOAT = "float", @@ -119,16 +65,6 @@ export enum DatabaseSerializedFieldType { FIELD_TRAVERSER = "field_traverser", } -export interface SerializedField { - key: string; - name: string; - type: DatabaseSerializedFieldType; - schema_valid: boolean; - fields?: string[]; - table?: string; - chain?: Array; -} - import { TableNodeImpl } from "./models"; export class Database { @@ -467,7 +403,7 @@ function constantTypeToSerializedFieldType( return null; } -export function serializeFields( +function serializeFields( fieldInput: Record, context: TSQLContext, tableChain: string[], diff --git a/internal-packages/tsql/src/query/escape.ts b/internal-packages/tsql/src/query/escape.ts index c0762d365bf..177e23b0557 100644 --- a/internal-packages/tsql/src/query/escape.ts +++ b/internal-packages/tsql/src/query/escape.ts @@ -46,7 +46,7 @@ export function safeIdentifier(identifier: string): string { * Escape a string value for use as a parameter in ClickHouse * Copied from clickhouse_driver.util.escape_param */ -export function escapeParamClickhouse(value: string): string { +function escapeParamClickhouse(value: string): string { const escaped = value .split("") .map((c) => singlequoteEscapeCharsMap[c] || c) diff --git a/internal-packages/tsql/src/query/models.ts b/internal-packages/tsql/src/query/models.ts index 49c8bd4f3c5..d28c1e184f9 100644 --- a/internal-packages/tsql/src/query/models.ts +++ b/internal-packages/tsql/src/query/models.ts @@ -15,19 +15,9 @@ export interface DatabaseField extends FieldOrTable { get_constant_type?(): ConstantType; default_value?(): any; } - -export interface IntegerDatabaseField extends DatabaseField {} -export interface FloatDatabaseField extends DatabaseField {} -export interface DecimalDatabaseField extends DatabaseField {} -export interface StringDatabaseField extends DatabaseField {} export interface UnknownDatabaseField extends DatabaseField {} -export interface StringJSONDatabaseField extends DatabaseField {} -export interface StringArrayDatabaseField extends DatabaseField {} -export interface FloatArrayDatabaseField extends DatabaseField {} -export interface DateDatabaseField extends DatabaseField {} export interface DateTimeDatabaseField extends DatabaseField {} export interface BooleanDatabaseField extends DatabaseField {} -export interface UUIDDatabaseField extends DatabaseField {} export interface ExpressionField extends DatabaseField { expr: Expr; @@ -57,14 +47,6 @@ export interface LazyTable extends Table {} export interface VirtualTable extends Table {} -export interface SavedQuery extends Table { - query: Expr; -} - -export interface FunctionCallTable extends Table { - call_function?(context: TSQLContext): Expr; -} - export interface TableNode { name: "root" | string; table?: FieldOrTable | null; @@ -238,16 +220,3 @@ export class TableNodeImpl implements TableNode { return start; } } - -export interface LazyTableToAdd { - lazy_table: LazyTable; - fields_accessed: Record>; -} - -export interface LazyJoinToAdd { - from_table: string; - to_table: string; - lazy_join: LazyJoin; - lazy_join_type: any; // LazyJoinType from ast.ts - fields_accessed: Record>; -} diff --git a/internal-packages/tsql/src/query/parse_string.ts b/internal-packages/tsql/src/query/parse_string.ts index 996c4e5494e..5e304be9fef 100644 --- a/internal-packages/tsql/src/query/parse_string.ts +++ b/internal-packages/tsql/src/query/parse_string.ts @@ -47,23 +47,3 @@ export function parseStringLiteralText(text: string): string { return replaceCommonEscapeCharacters(result); } - -export function parseStringLiteralCtx(ctx: { getText(): string }): string { - /** Converts a STRING_LITERAL received from antlr via ctx.getText() into a JavaScript string */ - const text = ctx.getText(); - return parseStringLiteralText(text); -} - -export function parseStringTextCtx( - ctx: { getText(): string }, - escapeQuotes: boolean = true -): string { - /** Converts a STRING_TEXT received from antlr via ctx.getText() into a JavaScript string */ - let text = ctx.getText(); - if (escapeQuotes) { - text = text.replace(/''/g, "'"); - text = text.replace(/\\'/g, "'"); - } - text = text.replace(/\\{/g, "{"); - return replaceCommonEscapeCharacters(text); -} diff --git a/internal-packages/tsql/src/query/property_types.ts b/internal-packages/tsql/src/query/property_types.ts index 8063dbd5ae2..a83221e440c 100644 --- a/internal-packages/tsql/src/query/property_types.ts +++ b/internal-packages/tsql/src/query/property_types.ts @@ -126,61 +126,6 @@ abstract class Visitor { } } -// TraversingVisitor - matches Python TraversingVisitor -class TraversingVisitor extends Visitor { - visitPropertyType(node: PropertyType): void { - this.visit(node.field_type); - } - - visitField(node: Field): void { - if (node.type) { - this.visit(node.type as any); - } - } - - visitCall(node: Call): void { - for (const arg of node.args) { - this.visit(arg); - } - if (node.params) { - for (const param of node.params) { - this.visit(param); - } - } - } - - visitConstant(node: Constant): void { - if (node.type) { - this.visit(node.type as any); - } - } - - // Default handler for unknown types - traverse common properties - visit_unknown(node: AST): void { - // Traverse children based on common AST node properties - if ("expr" in node) { - this.visit((node as any).expr); - } - if ("exprs" in node) { - for (const expr of (node as any).exprs) { - this.visit(expr); - } - } - if ("left" in node && "right" in node) { - this.visit((node as any).left); - this.visit((node as any).right); - } - if ("args" in node) { - for (const arg of (node as any).args) { - this.visit(arg); - } - } - if ("type" in node) { - this.visit((node as any).type); - } - } -} - // CloningVisitor - matches Python CloningVisitor class CloningVisitor extends Visitor { protected clearTypes: boolean; @@ -261,96 +206,6 @@ class CloningVisitor extends Visitor { } } -// PropertyFinder: Traverses AST to find all property references -class PropertyFinder extends TraversingVisitor { - context: TSQLContext; - personProperties: Set = new Set(); - eventProperties: Set = new Set(); - groupProperties: Map> = new Map(); - foundTimestamps: boolean = false; - - constructor(context: TSQLContext) { - super(); - this.context = context; - } - - visitPropertyType(node: PropertyType): void { - if (node.field_type.name === "properties" && node.chain.length === 1) { - const tableType = node.field_type.table_type; - if (this.isBaseTableType(tableType)) { - const table = tableType.resolve_database_table?.(this.context); - if (table) { - const tableName = table.to_printed_tsql?.() || ""; - const propertyName = String(node.chain[0]); - - if (tableName === "persons" || tableName === "raw_persons") { - this.personProperties.add(propertyName); - } else if (tableName === "groups") { - if (this.isLazyJoinType(tableType)) { - if (tableType.field.startsWith("group_")) { - const groupId = parseInt(tableType.field.split("_")[1], 10); - if (!this.groupProperties.has(groupId)) { - this.groupProperties.set(groupId, new Set()); - } - this.groupProperties.get(groupId)!.add(propertyName); - } - } else if (this.isLazyTableType(tableType)) { - const globalGroupId = this.context.globals?.group_id; - if (typeof globalGroupId === "number") { - if (!this.groupProperties.has(globalGroupId)) { - this.groupProperties.set(globalGroupId, new Set()); - } - this.groupProperties.get(globalGroupId)!.add(propertyName); - } - } - } else if (tableName === "events") { - if (this.isVirtualTableType(tableType) && tableType.field === "poe") { - this.personProperties.add(propertyName); - } else { - this.eventProperties.add(propertyName); - } - } - } - } - } - super.visitPropertyType(node); - } - - visitField(node: Field): void { - super.visitField(node); - if (this.isFieldType(node.type)) { - const dbField = (node.type as any).resolve_database_field?.(this.context); - if (this.isDateTimeDatabaseField(dbField)) { - this.foundTimestamps = true; - } - } - } - - private isBaseTableType(type: any): type is BaseTableType { - return type && typeof type.resolve_database_table === "function"; - } - - private isLazyJoinType(type: any): type is LazyJoinType { - return type && "lazy_join" in type && "field" in type; - } - - private isLazyTableType(type: any): type is LazyTableType { - return type && "table" in type && !("lazy_join" in type); - } - - private isVirtualTableType(type: any): type is VirtualTableType { - return type && "virtual_table" in type && "field" in type; - } - - private isFieldType(type: any): type is FieldType { - return type && typeof type.resolve_database_field === "function"; - } - - private isDateTimeDatabaseField(field: any): field is DateTimeDatabaseField { - return field && "name" in field; // Simplified check - } -} - // PropertySwapper: Transforms property accesses with type conversions export class PropertySwapper extends CloningVisitor { timezone: string; @@ -669,52 +524,3 @@ export class PropertySwapper extends CloningVisitor { return field && "name" in field; // Simplified check } } - -// Main function to build property swapper -export function buildPropertySwapper(node: AST, context: TSQLContext): void { - if (!context || !context.team_id) { - return; - } - - // NOTE: In TypeScript, you'll need to fetch the team from your database/ORM - // This is a placeholder - replace with your actual team fetching logic - // if (!context.team) { - // context.team = await Team.findById(context.team_id); - // } - - if (!context.team) { - return; - } - - // Find all properties - const propertyFinder = new PropertyFinder(context); - propertyFinder.visit(node); - - // NOTE: In TypeScript, you'll need to query PropertyDefinition from your database - // This is a placeholder - replace with your actual property definition fetching logic - // const eventPropertyValues = await PropertyDefinition.find({ - // project_id: context.team.project_id, - // name: { $in: Array.from(propertyFinder.eventProperties) }, - // type: { $in: [null, 'event'] }, - // }).select('name property_type'); - // const eventProperties = new Map( - // eventPropertyValues.filter((p: any) => p.property_type).map((p: any) => [p.name, p.property_type]) - // ); - - const eventProperties = new Map(); - const personProperties = new Map(); - const groupProperties = new Map(); - - // TODO: Implement actual property definition fetching from database - // For now, these are empty maps - - const timezone = (context.database as any)?._timezone || "UTC"; - context.property_swapper = new PropertySwapper( - timezone, - eventProperties, - personProperties, - groupProperties, - context, - true - ); -} diff --git a/internal-packages/tsql/src/query/schema.ts b/internal-packages/tsql/src/query/schema.ts index 0d50c1fbe3c..06f8def6882 100644 --- a/internal-packages/tsql/src/query/schema.ts +++ b/internal-packages/tsql/src/query/schema.ts @@ -613,13 +613,6 @@ export function validateGroupColumn( return col; } -/** - * Get the actual ClickHouse column name (handles aliasing) - */ -export function getClickHouseColumnName(col: ColumnSchema): string { - return col.clickhouseName ?? col.name; -} - /** * Check if a column is a virtual (computed) column * @@ -825,22 +818,6 @@ export function getInternalValueFromMappingCaseInsensitive( return null; } -/** - * Get all column names available for autocomplete - */ -export function getTableColumnNames(schema: SchemaRegistry, tableName: string): string[] { - const table = findTable(schema, tableName); - if (!table) return []; - return Object.keys(table.columns); -} - -/** - * Get all table names available for autocomplete - */ -export function getAllTableNames(schema: SchemaRegistry): string[] { - return Object.keys(schema.tables); -} - /** * Get the names of core columns for a table. * diff --git a/internal-packages/webhook-engine/src/engine/filter/index.ts b/internal-packages/webhook-engine/src/engine/filter/index.ts index b2c741547ef..20f728523bd 100644 --- a/internal-packages/webhook-engine/src/engine/filter/index.ts +++ b/internal-packages/webhook-engine/src/engine/filter/index.ts @@ -1,8 +1,3 @@ export { parseFilter } from "./parse.js"; export { evaluateFilter } from "./evaluate.js"; -export { - FilterParseError, - MAX_FILTER_CLAUSES, - type FilterContext, - type FilterMatch, -} from "./types.js"; +export { FilterParseError, type FilterContext } from "./types.js"; diff --git a/internal-packages/webhook-engine/src/engine/partitions.ts b/internal-packages/webhook-engine/src/engine/partitions.ts index 399f6cce8d5..d6bad650d35 100644 --- a/internal-packages/webhook-engine/src/engine/partitions.ts +++ b/internal-packages/webhook-engine/src/engine/partitions.ts @@ -3,8 +3,8 @@ import type { WebhookDatabase } from "@trigger.dev/database"; // Two identifier forms for the PascalCase Prisma table name. DDL must DOUBLE-QUOTE // (Postgres folds unquoted identifiers to lowercase); pg_class.relname stores the // bare case-preserved name, so catalog lookups bind the bare form. -export const PARENT_DDL = `"WebhookDelivery"`; -export const PARENT_NAME = `WebhookDelivery`; +const PARENT_DDL = `"WebhookDelivery"`; +const PARENT_NAME = `WebhookDelivery`; // --------------------------------------------------------------------------- // Day-bucket math (everything in UTC, matching how the migration writes bounds) @@ -34,7 +34,7 @@ export function dayBucket(lo: Date): Bucket { } /** All day buckets covering [start, end] inclusive of the day containing end. */ -export function dayBuckets(start: Date, end: Date): Bucket[] { +function dayBuckets(start: Date, end: Date): Bucket[] { const out: Bucket[] = []; let cur = floorDayUTC(start); const last = floorDayUTC(end); @@ -203,7 +203,7 @@ export type PartitionInfo = { hi?: Date; }; -export async function listPartitions(prisma: WebhookDatabase): Promise { +async function listPartitions(prisma: WebhookDatabase): Promise { const rows = await prisma.$queryRawUnsafe< { name: string; bound: string; approx_rows: bigint; bytes: bigint }[] >( diff --git a/internal-packages/webhook-engine/src/engine/verification/parse.ts b/internal-packages/webhook-engine/src/engine/verification/parse.ts index 70eaa2a779c..ba588d2fc50 100644 --- a/internal-packages/webhook-engine/src/engine/verification/parse.ts +++ b/internal-packages/webhook-engine/src/engine/verification/parse.ts @@ -28,7 +28,7 @@ export type PreparedVerification = // Parse one signature header into (a) candidate signature strings and (b) a field map for // signatureField lookups (e.g. Stripe `t`). See WebhookSignatureExtraction for the shapes. -export function parseSignatureHeader( +function parseSignatureHeader( headerValue: string, extraction?: WebhookSignatureExtraction ): { signatures: string[]; fields: Map } { diff --git a/knip.json b/knip.json index 51bddfe4751..f992e77c80f 100644 --- a/knip.json +++ b/knip.json @@ -1,4 +1,94 @@ { "$schema": "https://unpkg.com/knip@6/schema.json", - "ignoreDependencies": ["non.geist"] + "tags": ["-knipignore"], + "workspaces": { + ".": { + "entry": ["scripts/**/*.{js,mjs,cjs,ts,mts,cts}"], + "ignoreDependencies": ["agentcrumbs", "eslint", "lefthook"], + "ignoreBinaries": ["infisical", "prisma"] + }, + "apps/supervisor": { + "ignoreUnresolved": ["dotenv/config"] + }, + "apps/webapp": { + "entry": [ + "evalite.config.ts", + "vitest.*.config.ts", + "evals/**/*.eval.ts", + "memory-leak-detector.js", + "prisma/populate.ts", + "scripts/**/*.{js,mjs,cjs,ts,mts,cts}", + "test/**/*.producer.ts", + "test/types/**/*.types.ts", + "test/setup/global-e2e-full-setup.ts", + "vite/node-globals-shim.js", + "app/v3/otlpTransformWorker.ts" + ], + "ignoreDependencies": ["@sentry/cli", "assert", "util"] + }, + "internal-packages/dashboard-agent": { + "entry": ["trigger.config.ts", "src/investigation-sweep.ts", "src/maintenance.ts"], + "ignoreBinaries": ["rg"] + }, + "internal-packages/emails": { + "ignoreDependencies": ["@react-email/ui"] + }, + "internal-packages/observability-map": { + "entry": ["src/index.ts", "fixtures/**/*.{js,mjs,cjs,ts,mts,cts,tsx}"] + }, + "internal-packages/otlp-importer": { + "ignoreDependencies": ["ts-proto"] + }, + "internal-packages/run-ops-database": { + "ignoreDependencies": ["@prisma/client", "prisma"] + }, + "internal-packages/sdk-compat-tests": { + "entry": ["src/fixtures/**/*.{js,mjs,cjs,ts,mts,cts,tsx}"] + }, + "internal-packages/testcontainers": { + "entry": ["scripts/**/*.{js,mjs,cjs,ts,mts,cts}"] + }, + "internal-packages/tsql": { + "ignoreBinaries": ["tail"] + }, + "internal-packages/webhook-sources": { + "entry": ["catalog/**/*.{js,mjs,cjs,ts,mts,cts}"] + }, + "packages/build": { + "ignoreFiles": ["src/**/*-cjs.cts"], + "ignoreDependencies": ["@typescript/typescript6"] + }, + "packages/cli-v3": { + "entry": [ + "src/index.ts", + "src/entryPoints/**/*.ts", + "src/**/*-cjs.cts", + "src/dev/devWatchdog.ts", + "src/shims/esm.ts" + ], + "ignoreDependencies": ["@epic-web/test-server", "execa", "find-up"], + "ignoreBinaries": ["xdg-open"] + }, + "packages/core": { + "ignoreFiles": ["src/**/*-cjs.cts"], + "ignoreDependencies": ["ai-v7"] + }, + "packages/react-hooks": { + "ignoreDependencies": ["@types/react-dom"] + }, + "packages/rsc": { + "ignoreFiles": ["src/**/*-cjs.cts"], + "ignoreDependencies": ["react", "react-dom"] + }, + "packages/schema-to-json": { + "ignoreDependencies": ["runtypes", "superstruct", "valibot"] + }, + "packages/trigger-sdk": { + "ignoreFiles": ["src/**/*-cjs.cts", "src/v3/index-browser.mts"], + "ignoreDependencies": ["ai-v7", "react"] + }, + "docs": { + "ignoreFiles": ["style.css"] + } + } } diff --git a/lefthook.yml b/lefthook.yml index 818dbb0eff0..ad7d328c55c 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -36,3 +36,15 @@ pre-push: echo "" exit 1 } + - name: knip + run: | + pnpm run knip || { + echo "" + echo "✖ Unused code or dependencies found. Run:" + echo "" + echo " pnpm run knip" + echo "" + echo " then remove the unused items or update knip.json and re-push." + echo "" + exit 1 + } diff --git a/package.json b/package.json index b8fb57aa42c..4b4b4c7d8e8 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "format:prisma": "pnpm --filter @trigger.dev/database run format:prisma && pnpm --filter @internal/run-ops-database run format:prisma", "lint": "oxlint", "lint:fix": "oxlint --fix", - "knip:deps": "knip --production --dependencies", + "knip": "knip --include files,exports,types,dependencies,unlisted,binaries,unresolved,catalog", "docker": "node scripts/docker.mjs -f docker/docker-compose.yml up -d --build --remove-orphans", "docker:stop": "node scripts/docker.mjs -f docker/docker-compose.yml stop", "docker:full": "node scripts/docker.mjs -f docker/docker-compose.yml -f docker/docker-compose.extras.yml up -d --build --remove-orphans", @@ -56,12 +56,10 @@ "storybook": "turbo run storybook" }, "devDependencies": { - "@manypkg/cli": "^0.19.2", "@playwright/test": "^1.36.2", "@trigger.dev/database": "workspace:*", "@types/node": "24.13.3", "@vitest/coverage-v8": "4.1.7", - "autoprefixer": "^10.4.12", "knip": "6.25.0", "lefthook": "^2.1.10", "oxfmt": "^0.54.0", @@ -71,15 +69,13 @@ "tsx": "^3.7.1", "turbo": "^1.13.4", "typescript": "catalog:", - "vite-tsconfig-paths": "^4.0.5", "vitest": "4.1.7" }, "packageManager": "pnpm@10.33.2", "dependencies": { "@changesets/cli": "2.26.2", "@remix-run/changelog-github": "^0.0.5", - "agentcrumbs": "^0.5.0", - "node-fetch": "2.6.x" + "agentcrumbs": "^0.5.0" }, "pnpm": { "patchedDependencies": { diff --git a/packages/build/package.json b/packages/build/package.json index 2ee4f8fec69..49f0cf44df0 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -82,15 +82,12 @@ "@trigger.dev/core": "workspace:4.5.11", "mlly": "^1.7.1", "pkg-types": "^1.1.3", - "resolve": "^1.22.8", "tinyglobby": "^0.2.2", "tsconfck": "3.1.3" }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.5", - "@types/resolve": "^1.20.6", "@typescript/typescript6": "6.0.2", - "esbuild": "^0.23.0", "rimraf": "6.0.1", "tshy": "^4.1.3", "tsx": "4.17.0", diff --git a/packages/build/src/version.ts b/packages/build/src/version.ts deleted file mode 100644 index 2e47a886828..00000000000 --- a/packages/build/src/version.ts +++ /dev/null @@ -1 +0,0 @@ -export const VERSION = "0.0.0"; diff --git a/packages/cli-v3/package.json b/packages/cli-v3/package.json index 1caa6cf16e1..1bf6d500527 100644 --- a/packages/cli-v3/package.json +++ b/packages/cli-v3/package.json @@ -52,21 +52,13 @@ }, "devDependencies": { "@epic-web/test-server": "^0.1.0", - "@types/eventsource": "^1.1.15", - "@types/gradient-string": "^1.1.2", "@types/ini": "^4.1.1", - "@types/object-hash": "3.0.6", - "@types/react": "^18.2.48", "@types/resolve": "^1.20.6", - "@types/rimraf": "^4.0.5", "@types/semver": "^7.5.0", "@types/source-map-support": "0.5.10", - "@types/ws": "^8.5.3", - "cpy-cli": "^5.0.0", "execa": "^8.0.1", "find-up": "^7.0.0", "rimraf": "^6.0.1", - "ts-essentials": "10.0.1", "tshy": "^4.1.3", "tsx": "4.17.0" }, @@ -89,12 +81,7 @@ "@modelcontextprotocol/sdk": "^1.25.2", "@opentelemetry/api": "1.9.1", "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/exporter-trace-otlp-http": "0.218.0", "@opentelemetry/instrumentation": "0.218.0", - "@opentelemetry/instrumentation-fetch": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-node": "2.7.1", - "@opentelemetry/semantic-conventions": "1.41.1", "@s2-dev/streamstore": "^0.25.0", "@trigger.dev/build": "workspace:4.5.11", "@trigger.dev/core": "workspace:4.5.11", @@ -114,11 +101,9 @@ "evt": "^2.4.13", "fast-npm-meta": "^0.2.2", "git-last-commit": "^1.0.1", - "gradient-string": "^2.0.2", "has-flag": "^5.0.1", "ignore": "^7.0.5", "import-in-the-middle": "3.0.1", - "import-meta-resolve": "^4.1.0", "ini": "^5.0.0", "json-stable-stringify": "^1.3.0", "jsonc-parser": "3.2.1", @@ -126,11 +111,9 @@ "minimatch": "^10.0.1", "mlly": "^1.7.1", "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", "resolve": "^1.22.8", "semver": "^7.5.0", @@ -141,10 +124,8 @@ "strip-ansi": "^7.1.0", "supports-color": "^10.0.0", "tar": "^7.5.13", - "tiny-invariant": "^1.2.0", "tinyexec": "^0.3.1", "tinyglobby": "^0.2.10", - "ws": "^8.18.0", "xdg-app-paths": "^8.3.0", "zod": "3.25.76", "zod-validation-error": "^1.5.0" diff --git a/packages/cli-v3/src/build/buildWorker.ts b/packages/cli-v3/src/build/buildWorker.ts index fc199142492..837a1760f49 100644 --- a/packages/cli-v3/src/build/buildWorker.ts +++ b/packages/cli-v3/src/build/buildWorker.ts @@ -24,7 +24,7 @@ import { logBuildWorkerStart } from "./buildWorkerLogging.js"; import { SdkVersionExtractor } from "./plugins.js"; import { spinner } from "../utilities/windows.js"; -export type BuildWorkerEventListener = { +type BuildWorkerEventListener = { onBundleStart?: () => void; onBundleComplete?: (result: BundleResult) => void; }; @@ -142,6 +142,7 @@ export async function buildWorker(options: BuildWorkerOptions) { return buildManifest; } +/** @knipignore Exported for the CLI end-to-end suite. */ export function rewriteBuildManifestPaths( buildManifest: BuildManifest, destinationDir: string diff --git a/packages/cli-v3/src/build/externals.ts b/packages/cli-v3/src/build/externals.ts index c35f90cf182..38d8e4cdf50 100644 --- a/packages/cli-v3/src/build/externals.ts +++ b/packages/cli-v3/src/build/externals.ts @@ -124,13 +124,13 @@ async function isExternalResolvable( } } -export type CollectedExternal = { +type CollectedExternal = { name: string; path: string; version: string; }; -export type ExternalsCollector = { +type ExternalsCollector = { externals: Array; plugin: esbuild.Plugin; }; diff --git a/packages/cli-v3/src/build/packageModules.ts b/packages/cli-v3/src/build/packageModules.ts index ada72e87739..20eec380f34 100644 --- a/packages/cli-v3/src/build/packageModules.ts +++ b/packages/cli-v3/src/build/packageModules.ts @@ -3,22 +3,18 @@ import { basename, dirname, join, resolve } from "node:path"; import { sourceDir } from "../sourceDir.js"; import { assertExhaustive } from "../utilities/assertExhaustive.js"; -export const devRunWorker = join(sourceDir, "entryPoints", "dev-run-worker.js"); -export const devIndexWorker = join(sourceDir, "entryPoints", "dev-index-worker.js"); - -export const managedRunController = join(sourceDir, "entryPoints", "managed-run-controller.js"); -export const managedRunWorker = join(sourceDir, "entryPoints", "managed-run-worker.js"); -export const managedIndexController = join(sourceDir, "entryPoints", "managed-index-controller.js"); -export const managedIndexWorker = join(sourceDir, "entryPoints", "managed-index-worker.js"); - -export const unmanagedRunController = join(sourceDir, "entryPoints", "unmanaged-run-controller.js"); -export const unmanagedRunWorker = join(sourceDir, "entryPoints", "unmanaged-run-worker.js"); -export const unmanagedIndexController = join( - sourceDir, - "entryPoints", - "unmanaged-index-controller.js" -); -export const unmanagedIndexWorker = join(sourceDir, "entryPoints", "unmanaged-index-worker.js"); +const devRunWorker = join(sourceDir, "entryPoints", "dev-run-worker.js"); +const devIndexWorker = join(sourceDir, "entryPoints", "dev-index-worker.js"); + +const managedRunController = join(sourceDir, "entryPoints", "managed-run-controller.js"); +const managedRunWorker = join(sourceDir, "entryPoints", "managed-run-worker.js"); +const managedIndexController = join(sourceDir, "entryPoints", "managed-index-controller.js"); +const managedIndexWorker = join(sourceDir, "entryPoints", "managed-index-worker.js"); + +const unmanagedRunController = join(sourceDir, "entryPoints", "unmanaged-run-controller.js"); +const unmanagedRunWorker = join(sourceDir, "entryPoints", "unmanaged-run-worker.js"); +const unmanagedIndexController = join(sourceDir, "entryPoints", "unmanaged-index-controller.js"); +const unmanagedIndexWorker = join(sourceDir, "entryPoints", "unmanaged-index-worker.js"); export const telemetryEntryPoint = join(sourceDir, "entryPoints", "loader.js"); @@ -36,7 +32,7 @@ export const unmanagedEntryPoints = [ unmanagedIndexWorker, ]; -export const esmShimPath = join(sourceDir, "shims", "esm.js"); +const esmShimPath = join(sourceDir, "shims", "esm.js"); export const shims = [esmShimPath]; @@ -232,7 +228,7 @@ export function getIndexControllerForTarget(target: BuildTarget) { } } -export function isConfigEntryPoint(entryPoint: string) { +function isConfigEntryPoint(entryPoint: string) { return entryPoint.startsWith("trigger.config.ts"); } diff --git a/packages/cli-v3/src/build/plugins.ts b/packages/cli-v3/src/build/plugins.ts index 81a1cecea46..1f945fa2a2b 100644 --- a/packages/cli-v3/src/build/plugins.ts +++ b/packages/cli-v3/src/build/plugins.ts @@ -32,7 +32,7 @@ export async function buildPlugins( return plugins; } -export function analyzeMetadataPlugin(): esbuild.Plugin { +function analyzeMetadataPlugin(): esbuild.Plugin { return { name: "analyze-metafile", setup(build) { @@ -58,7 +58,7 @@ const polysheds = [ }, ]; -export function polyshedPlugin(): esbuild.Plugin { +function polyshedPlugin(): esbuild.Plugin { return { name: "polyshed", setup(build) { diff --git a/packages/cli-v3/src/commands/analyze.ts b/packages/cli-v3/src/commands/analyze.ts index dc03d36e2e5..f6503d77987 100644 --- a/packages/cli-v3/src/commands/analyze.ts +++ b/packages/cli-v3/src/commands/analyze.ts @@ -37,14 +37,14 @@ export function configureAnalyzeCommand(program: Command) { }); } -export async function analyzeCommand(dir: string | undefined, options: unknown) { +async function analyzeCommand(dir: string | undefined, options: unknown) { return await wrapCommandAction("analyze", AnalyzeOptions, options, async (opts) => { await printInitialBanner(false); return await analyze(dir, opts); }); } -export async function analyze(dir: string | undefined, options: AnalyzeOptions) { +async function analyze(dir: string | undefined, options: AnalyzeOptions) { const cwd = process.cwd(); const targetDir = dir ? path.resolve(cwd, dir) : cwd; const metafilePath = path.join(targetDir, "metafile.json"); diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 41c48330836..ae8cc3ee5a8 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -249,7 +249,7 @@ export function configureDeployCommand(program: Command) { ); } -export async function deployCommand(dir: string, options: unknown) { +async function deployCommand(dir: string, options: unknown) { return await wrapCommandAction("deployCommand", DeployCommandOptions, options, async (opts) => { return await _deployCommand(dir, opts); }); @@ -778,7 +778,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { }); } -export async function syncEnvVarsWithServer( +async function syncEnvVarsWithServer( apiClient: CliApiClient, projectRef: string, environmentSlug: string, diff --git a/packages/cli-v3/src/commands/dev.ts b/packages/cli-v3/src/commands/dev.ts index 9daf07532ce..a2e9527bac1 100644 --- a/packages/cli-v3/src/commands/dev.ts +++ b/packages/cli-v3/src/commands/dev.ts @@ -154,7 +154,7 @@ export function configureDevCommand(program: Command) { }); } -export async function devCommand(options: DevCommandOptions) { +async function devCommand(options: DevCommandOptions) { runtimeChecks(); // Only show these install prompts if the user is in a terminal (not in a Coding Agent) diff --git a/packages/cli-v3/src/commands/init.ts b/packages/cli-v3/src/commands/init.ts index 46aeee5f5e0..59be2876c06 100644 --- a/packages/cli-v3/src/commands/init.ts +++ b/packages/cli-v3/src/commands/init.ts @@ -123,7 +123,7 @@ Examples: }); } -export async function initCommand(dir: string, options: unknown) { +async function initCommand(dir: string, options: unknown) { return await wrapCommandAction("initCommand", InitCommandOptions, options, async (opts) => { return await _initCommand(dir, opts); }); @@ -595,7 +595,7 @@ async function addConfigFileToTsConfig(tsconfigPath: string, options: InitComman }); } -export interface InstallPackagesOutputter { +interface InstallPackagesOutputter { startSDK: () => void; installedSDK: () => void; startBuild: () => void; @@ -648,7 +648,7 @@ class SilentInstallPackagesOutputter implements InstallPackagesOutputter { stoppedWithError() {} } -export async function installPackages( +async function installPackages( projectDir: string, tag: string, outputter: InstallPackagesOutputter = new SilentInstallPackagesOutputter() diff --git a/packages/cli-v3/src/commands/install-mcp.ts b/packages/cli-v3/src/commands/install-mcp.ts index d2e19380dea..f4e18874794 100644 --- a/packages/cli-v3/src/commands/install-mcp.ts +++ b/packages/cli-v3/src/commands/install-mcp.ts @@ -161,7 +161,7 @@ export function configureInstallMcpCommand(program: Command) { }); } -export async function installMcpCommand(options: unknown) { +async function installMcpCommand(options: unknown) { return await wrapCommandAction( "installMcpCommand", InstallMcpCommandOptions, diff --git a/packages/cli-v3/src/commands/list-profiles.ts b/packages/cli-v3/src/commands/list-profiles.ts index c4490b5d10b..1a5ddc2e5e4 100644 --- a/packages/cli-v3/src/commands/list-profiles.ts +++ b/packages/cli-v3/src/commands/list-profiles.ts @@ -31,14 +31,14 @@ export function configureListProfilesCommand(program: Command) { }); } -export async function listProfilesCommand(options: unknown) { +async function listProfilesCommand(options: unknown) { return await wrapCommandAction("listProfiles", ListProfilesOptions, options, async (opts) => { await printInitialBanner(false); return await listProfiles(opts); }); } -export async function listProfiles(options: ListProfilesOptions) { +async function listProfiles(options: ListProfilesOptions) { const authConfig = readAuthConfigFile(); if (!authConfig) { diff --git a/packages/cli-v3/src/commands/login.ts b/packages/cli-v3/src/commands/login.ts index 83576b0d010..1f19655a77d 100644 --- a/packages/cli-v3/src/commands/login.ts +++ b/packages/cli-v3/src/commands/login.ts @@ -39,12 +39,12 @@ import { } from "../utilities/accessTokens.js"; import { links } from "@trigger.dev/core/v3"; -export const LoginCommandOptions = CommonCommandOptions.extend({ +const LoginCommandOptions = CommonCommandOptions.extend({ apiUrl: z.string(), browser: z.boolean().default(true), }); -export type LoginCommandOptions = z.infer; +type LoginCommandOptions = z.infer; export function configureLoginCommand(program: Command) { return commonOptions( @@ -75,7 +75,7 @@ Examples: }); } -export async function loginCommand(options: unknown) { +async function loginCommand(options: unknown) { return await wrapCommandAction("loginCommand", LoginCommandOptions, options, async (opts) => { return await _loginCommand(opts); }); diff --git a/packages/cli-v3/src/commands/logout.ts b/packages/cli-v3/src/commands/logout.ts index 6250d98d516..6464307215f 100644 --- a/packages/cli-v3/src/commands/logout.ts +++ b/packages/cli-v3/src/commands/logout.ts @@ -25,13 +25,13 @@ export function configureLogoutCommand(program: Command) { ); } -export async function logoutCommand(options: unknown) { +async function logoutCommand(options: unknown) { return await wrapCommandAction("logoutCommand", LogoutCommandOptions, options, async (opts) => { return await logout(opts); }); } -export async function logout(options: LogoutCommandOptions) { +async function logout(options: LogoutCommandOptions) { const config = readAuthConfigProfile(options.profile); if (!config?.accessToken) { diff --git a/packages/cli-v3/src/commands/mcp.ts b/packages/cli-v3/src/commands/mcp.ts index cc5d951eb24..923e85d2736 100644 --- a/packages/cli-v3/src/commands/mcp.ts +++ b/packages/cli-v3/src/commands/mcp.ts @@ -53,7 +53,7 @@ export function configureMcpCommand(program: Command) { }); } -export async function mcpCommand(options: McpCommandOptions) { +async function mcpCommand(options: McpCommandOptions) { // The install wizard runs ONLY when explicitly requested (`trigger mcp --install`). // Bare `trigger mcp` always starts the server — MCP hosts (e.g. Claude Code) spawn it // over a PTY, so `process.stdout.isTTY` is true even though no human is there; gating diff --git a/packages/cli-v3/src/commands/mint-token.ts b/packages/cli-v3/src/commands/mint-token.ts index 8d847dba3f6..125f3b561a1 100644 --- a/packages/cli-v3/src/commands/mint-token.ts +++ b/packages/cli-v3/src/commands/mint-token.ts @@ -37,7 +37,7 @@ export function configureMintTokenCommand(program: Command) { }); } -export async function mintTokenCommand(options: unknown) { +async function mintTokenCommand(options: unknown) { return await wrapCommandAction( "mintTokenCommand", MintTokenCommandOptions, diff --git a/packages/cli-v3/src/commands/preview.ts b/packages/cli-v3/src/commands/preview.ts index ed74656f044..30b35740fad 100644 --- a/packages/cli-v3/src/commands/preview.ts +++ b/packages/cli-v3/src/commands/preview.ts @@ -58,7 +58,7 @@ export function configurePreviewCommand(program: Command) { }); } -export async function previewArchiveCommand(dir: string, options: unknown) { +async function previewArchiveCommand(dir: string, options: unknown) { return await wrapCommandAction( "previewArchiveCommand", PreviewCommandOptions, diff --git a/packages/cli-v3/src/commands/promote.ts b/packages/cli-v3/src/commands/promote.ts index b7a9e7a0824..2360648b99e 100644 --- a/packages/cli-v3/src/commands/promote.ts +++ b/packages/cli-v3/src/commands/promote.ts @@ -55,7 +55,7 @@ export function configurePromoteCommand(program: Command) { }); } -export async function promoteCommand(version: string, options: unknown) { +async function promoteCommand(version: string, options: unknown) { return await wrapCommandAction("promoteCommand", PromoteCommandOptions, options, async (opts) => { return await _promoteCommand(version, opts); }); diff --git a/packages/cli-v3/src/commands/skills.ts b/packages/cli-v3/src/commands/skills.ts index 2865865f0c7..ef53ee5d25f 100644 --- a/packages/cli-v3/src/commands/skills.ts +++ b/packages/cli-v3/src/commands/skills.ts @@ -81,7 +81,7 @@ export function configureSkillsCommand(program: Command) { }); } -export async function installSkillsCommand(options: unknown) { +async function installSkillsCommand(options: unknown) { return await wrapCommandAction( "installSkillsCommand", SkillsCommandOptions, diff --git a/packages/cli-v3/src/commands/switch.ts b/packages/cli-v3/src/commands/switch.ts index 62703079566..3b88467346e 100644 --- a/packages/cli-v3/src/commands/switch.ts +++ b/packages/cli-v3/src/commands/switch.ts @@ -38,14 +38,14 @@ export function configureSwitchProfilesCommand(program: Command) { }); } -export async function switchProfilesCommand(profile: string | undefined, options: unknown) { +async function switchProfilesCommand(profile: string | undefined, options: unknown) { return await wrapCommandAction("switch", SwitchProfilesOptions, options, async (opts) => { await printInitialBanner(false); return await switchProfiles(profile, opts); }); } -export async function switchProfiles(profile: string | undefined, options: SwitchProfilesOptions) { +async function switchProfiles(profile: string | undefined, options: SwitchProfilesOptions) { intro("Switch profiles"); const authConfig = readAuthConfigFile(); diff --git a/packages/cli-v3/src/commands/trigger.ts b/packages/cli-v3/src/commands/trigger.ts deleted file mode 100644 index 7ab615dc383..00000000000 --- a/packages/cli-v3/src/commands/trigger.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { intro, outro } from "@clack/prompts"; -import type { Command } from "commander"; -import { z } from "zod"; -import { CommonCommandOptions, handleTelemetry, wrapCommandAction } from "../cli/common.js"; -import { printInitialBanner } from "../utilities/initialBanner.js"; -import { logger } from "../utilities/logger.js"; -import { resolve } from "path"; -import { loadConfig } from "../config.js"; -import { getProjectClient } from "../utilities/session.js"; -import { login } from "./login.js"; -import { chalkGrey, chalkLink, cliLink } from "../utilities/cliOutput.js"; - -const TriggerTaskOptions = CommonCommandOptions.extend({ - env: z.enum(["prod", "staging"]), - config: z.string().optional(), - projectRef: z.string().optional(), -}); - -type TriggerTaskOptions = z.infer; - -export function configureTriggerTaskCommand(program: Command) { - return program - .command("trigger") - .description("Trigger a task") - .argument("[task-name]", "The name of the task") - .option( - "-l, --log-level ", - "The CLI log level to use (debug, info, log, warn, error, none). This does not effect the log level of your trigger.dev tasks.", - "log" - ) - .option("--skip-telemetry", "Opt-out of sending telemetry") - .option( - "-e, --env ", - "Deploy to a specific environment (currently only prod and staging are supported)", - "prod" - ) - .option("-c, --config ", "The name of the config file, found at [path]") - .option( - "-p, --project-ref ", - "The project ref. Required if there is no config file. This will override the project specified in the config file." - ) - .action(async (path, options) => { - await handleTelemetry(async () => { - await triggerTaskCommand(path, options); - }); - }); -} - -export async function triggerTaskCommand(taskName: string, options: unknown) { - return await wrapCommandAction("trigger", TriggerTaskOptions, options, async (opts) => { - await printInitialBanner(false, opts.profile); - return await triggerTask(taskName, opts); - }); -} - -export async function triggerTask(taskName: string, options: TriggerTaskOptions) { - if (!taskName) { - throw new Error("You must provide a task name"); - } - - intro(`Triggering task ${taskName}`); - - const authorization = await login({ - embedded: true, - defaultApiUrl: options.apiUrl, - profile: options.profile, - silent: true, - }); - - if (!authorization.ok) { - if (authorization.error === "fetch failed") { - throw new Error( - `Failed to connect to ${authorization.auth?.apiUrl}. Are you sure it's the correct URL?` - ); - } else { - throw new Error( - `You must login first. Use the \`login\` CLI command.\n\n${authorization.error}` - ); - } - } - - const projectPath = resolve(process.cwd(), "."); - - const resolvedConfig = await loadConfig({ - cwd: projectPath, - overrides: { project: options.projectRef }, - configFile: options.config, - }); - - logger.debug("Resolved config", resolvedConfig); - - const projectClient = await getProjectClient({ - accessToken: authorization.auth.accessToken, - apiUrl: authorization.auth.apiUrl, - projectRef: resolvedConfig.project, - env: options.env, - profile: options.profile, - }); - - if (!projectClient) { - throw new Error("Failed to get project client"); - } - - const triggered = await projectClient.client.triggerTaskRun(taskName, { - payload: { - message: "Triggered by CLI", - }, - }); - - if (!triggered.success) { - throw new Error("Failed to trigger task"); - } - - const baseUrl = `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project}`; - const runUrl = `${baseUrl}/runs/${triggered.data.id}`; - - const pipe = chalkGrey("|"); - const link = chalkLink(cliLink("View run", runUrl)); - - outro(`Success! ${pipe} ${link}`); -} diff --git a/packages/cli-v3/src/commands/update.ts b/packages/cli-v3/src/commands/update.ts index 4c8d628b12e..88a9c28aed3 100644 --- a/packages/cli-v3/src/commands/update.ts +++ b/packages/cli-v3/src/commands/update.ts @@ -48,7 +48,7 @@ export function configureUpdateCommand(program: Command) { const triggerPackageFilter = /^@trigger\.dev/; -export async function updateCommand(dir: string, options: UpdateCommandOptions) { +async function updateCommand(dir: string, options: UpdateCommandOptions) { await updateTriggerPackages(dir, options, false); } diff --git a/packages/cli-v3/src/commands/whoami.ts b/packages/cli-v3/src/commands/whoami.ts index 406b18f83ee..3ce12904e63 100644 --- a/packages/cli-v3/src/commands/whoami.ts +++ b/packages/cli-v3/src/commands/whoami.ts @@ -62,7 +62,7 @@ export function configureWhoamiCommand(program: Command) { }); } -export async function whoAmICommand(options: unknown) { +async function whoAmICommand(options: unknown) { return await wrapCommandAction("whoamiCommand", WhoamiCommandOptions, options, async (opts) => { return await whoAmI(opts); }); diff --git a/packages/cli-v3/src/commands/workers/build.ts b/packages/cli-v3/src/commands/workers/build.ts deleted file mode 100644 index cea001fe99b..00000000000 --- a/packages/cli-v3/src/commands/workers/build.ts +++ /dev/null @@ -1,603 +0,0 @@ -import { intro, log, outro } from "@clack/prompts"; -import { getBranch, prepareDeploymentError } from "@trigger.dev/core/v3"; -import type { InitializeDeploymentResponseBody } from "@trigger.dev/core/v3/schemas"; -import type { Command } from "commander"; -import { Option as CommandOption } from "commander"; -import { resolve } from "node:path"; -import { z } from "zod"; -import type { CliApiClient } from "../../apiClient.js"; -import { buildWorker } from "../../build/buildWorker.js"; -import { resolveAlwaysExternal } from "../../build/externals.js"; -import { - CommonCommandOptions, - commonOptions, - handleTelemetry, - SkipLoggingError, - wrapCommandAction, -} from "../../cli/common.js"; -import { loadConfig } from "../../config.js"; -import { buildImage } from "../../deploy/buildImage.js"; -import { - checkLogsForErrors, - checkLogsForWarnings, - printErrors, - printWarnings, - saveLogs, -} from "../../deploy/logs.js"; -import { chalkError, cliLink, isLinksSupported, prettyError } from "../../utilities/cliOutput.js"; -import { loadDotEnvVars } from "../../utilities/dotEnv.js"; -import { createGitMeta } from "../../utilities/gitMeta.js"; -import { printStandloneInitialBanner } from "../../utilities/initialBanner.js"; -import { logger } from "../../utilities/logger.js"; -import { getProjectClient } from "../../utilities/session.js"; -import { getTmpDir } from "../../utilities/tempDirectories.js"; -import { spinner } from "../../utilities/windows.js"; -import { login } from "../login.js"; -import { updateTriggerPackages } from "../update.js"; - -const WorkersBuildCommandOptions = CommonCommandOptions.extend({ - // docker build options - load: z.boolean().default(false), - network: z.enum(["default", "none", "host"]).optional(), - tag: z.string().optional(), - push: z.boolean().default(false), - noCache: z.boolean().default(false), - // trigger options - local: z.boolean().default(false), // TODO: default to true when webapp has no remote build support - dryRun: z.boolean().default(false), - skipSyncEnvVars: z.boolean().default(false), - env: z.enum(["prod", "staging", "preview"]), - branch: z.string().optional(), - config: z.string().optional(), - projectRef: z.string().optional(), - apiUrl: z.string().optional(), - saveLogs: z.boolean().default(false), - skipUpdateCheck: z.boolean().default(false), - envFile: z.string().optional(), -}); - -type WorkersBuildCommandOptions = z.infer; - -type Deployment = InitializeDeploymentResponseBody; - -export function configureWorkersBuildCommand(program: Command) { - return commonOptions( - program - .command("build") - .description("Build a self-hosted worker image") - .argument("[path]", "The path to the project", ".") - .option( - "-e, --env ", - "Deploy to a specific environment (currently only prod and staging are supported)", - "prod" - ) - .option( - "-b, --branch ", - "The branch to deploy to. If not provided, the branch will be detected from the current git branch." - ) - .option("--skip-update-check", "Skip checking for @trigger.dev package updates") - .option("-c, --config ", "The name of the config file, found at [path]") - .option( - "-p, --project-ref ", - "The project ref. Required if there is no config file. This will override the project specified in the config file." - ) - .option( - "--skip-sync-env-vars", - "Skip syncing environment variables when using the syncEnvVars extension." - ) - .option( - "--env-file ", - "Path to the .env file to load into the CLI process. Defaults to .env in the project directory." - ) - ) - .addOption( - new CommandOption( - "--dry-run", - "This will only create the build context without actually building the image. This can be useful for debugging." - ).hideHelp() - ) - .addOption( - new CommandOption( - "--no-cache", - "Do not use any build cache. This will significantly slow down the build process but can be useful to fix caching issues." - ).hideHelp() - ) - .option("--local", "Force building the image locally.") - .option("--push", "Push the image to the configured registry.") - .option( - "-t, --tag ", - "Specify the full name of the resulting image with an optional tag. The tag will always be overridden for remote builds." - ) - .option("--load", "Load the built image into your local docker") - .option( - "--network ", - "The networking mode for RUN instructions when using --local", - "host" - ) - .option( - "--platform ", - "The platform to build the deployment image for", - "linux/amd64" - ) - .option("--save-logs", "If provided, will save logs even for successful builds") - .action(async (path, options) => { - await handleTelemetry(async () => { - await printStandloneInitialBanner(true, options.profile); - await workersBuildCommand(path, options); - }); - }); -} - -async function workersBuildCommand(dir: string, options: unknown) { - return await wrapCommandAction( - "workerBuildCommand", - WorkersBuildCommandOptions, - options, - async (opts) => { - return await _workerBuildCommand(dir, opts); - } - ); -} - -async function _workerBuildCommand(dir: string, options: WorkersBuildCommandOptions) { - intro("Building worker image"); - - if (!options.skipUpdateCheck) { - await updateTriggerPackages(dir, { ...options }, true, true); - } - - const projectPath = resolve(process.cwd(), dir); - - const authorization = await login({ - embedded: true, - defaultApiUrl: options.apiUrl, - profile: options.profile, - }); - - if (!authorization.ok) { - if (authorization.error === "fetch failed") { - throw new Error( - `Failed to connect to ${authorization.auth?.apiUrl}. Are you sure it's the correct URL?` - ); - } else { - throw new Error( - `You must login first. Use the \`login\` CLI command.\n\n${authorization.error}` - ); - } - } - - const resolvedConfig = await loadConfig({ - cwd: projectPath, - overrides: { project: options.projectRef }, - configFile: options.config, - }); - - logger.debug("Resolved config", resolvedConfig); - - const gitMeta = await createGitMeta(resolvedConfig.workspaceDir); - logger.debug("gitMeta", gitMeta); - - const branch = - options.env === "preview" ? getBranch({ specified: options.branch, gitMeta }) : undefined; - if (options.env === "preview" && !branch) { - throw new Error( - "You need to specify a preview branch when deploying to preview, pass --branch ." - ); - } - - const projectClient = await getProjectClient({ - accessToken: authorization.auth.accessToken, - apiUrl: authorization.auth.apiUrl, - projectRef: resolvedConfig.project, - env: options.env, - branch, - profile: options.profile, - }); - - if (!projectClient) { - throw new Error("Failed to get project client"); - } - - const serverEnvVars = await projectClient.client.getEnvironmentVariables(resolvedConfig.project); - loadDotEnvVars(resolvedConfig.workingDir, options.envFile); - - const destination = getTmpDir(resolvedConfig.workingDir, "build", options.dryRun); - - const $buildSpinner = spinner(); - - const forcedExternals = await resolveAlwaysExternal(projectClient.client); - - const buildManifest = await buildWorker({ - target: "unmanaged", - environment: options.env, - branch, - destination: destination.path, - resolvedConfig, - rewritePaths: true, - envVars: serverEnvVars.success ? serverEnvVars.data.variables : {}, - forcedExternals, - listener: { - onBundleStart() { - $buildSpinner.start("Building project"); - }, - onBundleComplete(result) { - $buildSpinner.stop("Successfully built project"); - - logger.debug("Bundle result", result); - }, - }, - }); - - logger.debug("Successfully built project to", destination.path); - - if (options.dryRun) { - logger.info(`Dry run complete. View the built project at ${destination.path}`); - return; - } - - const deploymentResponse = await projectClient.client.initializeDeployment({ - contentHash: buildManifest.contentHash, - userId: authorization.userId, - selfHosted: options.local, - type: "UNMANAGED", - isNativeBuild: false, - }); - - if (!deploymentResponse.success) { - throw new Error(`Failed to start deployment: ${deploymentResponse.error}`); - } - - const deployment = deploymentResponse.data; - - let local = options.local; - - // If the deployment doesn't have any externalBuildData, then we can't use the remote image builder - if (!deployment.externalBuildData && !options.local) { - log.warn( - "This webapp instance does not support remote builds, falling back to local build. Please use the `--local` flag to skip this warning." - ); - local = true; - } - - const childVars = buildManifest.deploy.sync?.env ?? {}; - const parentVars = buildManifest.deploy.sync?.parentEnv ?? {}; - const secretChildVars = buildManifest.deploy.sync?.secretEnv ?? {}; - const secretParentVars = buildManifest.deploy.sync?.secretParentEnv ?? {}; - - const hasVarsToSync = - Object.keys(childVars).length > 0 || - Object.keys(secretChildVars).length > 0 || - // Only sync parent variables if this is a branch environment - (branch && (Object.keys(parentVars).length > 0 || Object.keys(secretParentVars).length > 0)); - - if (hasVarsToSync) { - const numberOfEnvVars = - Object.keys(childVars).length + - Object.keys(parentVars).length + - Object.keys(secretChildVars).length + - Object.keys(secretParentVars).length; - const vars = numberOfEnvVars === 1 ? "var" : "vars"; - - if (!options.skipSyncEnvVars) { - const $spinner = spinner(); - $spinner.start(`Syncing ${numberOfEnvVars} env ${vars} with the server`); - const success = await syncEnvVarsWithServer( - projectClient.client, - resolvedConfig.project, - options.env, - childVars, - parentVars, - secretChildVars, - secretParentVars - ); - - if (!success) { - await failDeploy( - projectClient.client, - deployment, - { - name: "SyncEnvVarsError", - message: `Failed to sync ${numberOfEnvVars} env ${vars} with the server`, - }, - "", - $spinner - ); - } else { - $spinner.stop(`Successfully synced ${numberOfEnvVars} env ${vars} with the server`); - } - } else { - logger.log( - "Skipping syncing env vars. The environment variables in your project have changed, but the --skip-sync-env-vars flag was provided." - ); - } - } - - const version = deployment.version; - - const deploymentLink = cliLink( - "View deployment", - `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project}/deployments/${deployment.shortCode}` - ); - - const testLink = cliLink( - "Test tasks", - `${authorization.dashboardUrl}/projects/v3/${resolvedConfig.project}/test?environment=${ - options.env === "prod" ? "prod" : "stg" - }` - ); - - const $spinner = spinner(); - - if (isLinksSupported) { - $spinner.start(`Building worker version ${version} ${deploymentLink}`); - } else { - $spinner.start(`Building worker version ${version}`); - } - - const buildResult = await buildImage({ - isLocalBuild: local, - imagePlatform: deployment.imagePlatform, - noCache: options.noCache, - push: options.push, - deploymentId: deployment.id, - deploymentVersion: deployment.version, - imageTag: deployment.imageTag, - load: options.load, - contentHash: deployment.contentHash, - externalBuildId: deployment.externalBuildData?.buildId, - externalBuildToken: deployment.externalBuildData?.buildToken, - externalBuildProjectId: deployment.externalBuildData?.projectId, - projectId: projectClient.id, - projectRef: resolvedConfig.project, - apiUrl: projectClient.client.apiURL, - apiKey: projectClient.client.accessToken!, - apiClient: projectClient.client, - branchName: branch, - authAccessToken: authorization.auth.accessToken, - compilationPath: destination.path, - buildEnvVars: buildManifest.build.env, - network: options.network, - builder: "trigger", - }); - - logger.debug("Build result", buildResult); - - const warnings = checkLogsForWarnings(buildResult.logs); - - if (!warnings.ok) { - await failDeploy( - projectClient.client, - deployment, - { name: "BuildError", message: warnings.summary }, - buildResult.logs, - $spinner, - warnings.warnings, - warnings.errors - ); - - throw new SkipLoggingError("Failed to build image"); - } - - if (!buildResult.ok) { - await failDeploy( - projectClient.client, - deployment, - { name: "BuildError", message: buildResult.error }, - buildResult.logs, - $spinner, - warnings.warnings - ); - - throw new SkipLoggingError("Failed to build image"); - } - - // Index the deployment - // const runtime = new UnmanagedWorkerRuntime({ - // name: projectClient.name, - // config: resolvedConfig, - // args: { - // ...options, - // debugOtel: false, - // }, - // client: projectClient.client, - // dashboardUrl: authorization.dashboardUrl, - // }); - // await runtime.init(); - - // console.log("buildManifest", buildManifest); - - // await runtime.initializeWorker(buildManifest); - - const getDeploymentResponse = await projectClient.client.getDeployment(deployment.id); - - if (!getDeploymentResponse.success) { - await failDeploy( - projectClient.client, - deployment, - { name: "DeploymentError", message: getDeploymentResponse.error }, - buildResult.logs, - $spinner - ); - - throw new SkipLoggingError("Failed to get deployment with worker"); - } - - const deploymentWithWorker = getDeploymentResponse.data; - - if (!deploymentWithWorker.worker) { - await failDeploy( - projectClient.client, - deployment, - { name: "DeploymentError", message: "Failed to get deployment with worker" }, - buildResult.logs, - $spinner - ); - - throw new SkipLoggingError("Failed to get deployment with worker"); - } - - $spinner.stop(`Successfully built worker version ${version}`); - - const taskCount = deploymentWithWorker.worker?.tasks.length ?? 0; - - log.message(`Detected ${taskCount} task${taskCount === 1 ? "" : "s"}`); - - if (taskCount > 0) { - logger.table( - deploymentWithWorker.worker.tasks.map((task) => ({ - id: task.slug, - export: task.exportName ?? "@deprecated", - path: task.filePath, - })) - ); - } - - outro( - `Version ${version} built and ready to deploy: ${deployment.imageTag} ${ - isLinksSupported ? `| ${deploymentLink} | ${testLink}` : "" - }` - ); -} - -export async function syncEnvVarsWithServer( - apiClient: CliApiClient, - projectRef: string, - environmentSlug: string, - envVars: Record, - parentEnvVars?: Record, - secretEnvVars?: Record, - secretParentEnvVars?: Record -) { - const hasNonSecret = - Object.keys(envVars).length > 0 || Object.keys(parentEnvVars ?? {}).length > 0; - const hasSecret = - Object.keys(secretEnvVars ?? {}).length > 0 || - Object.keys(secretParentEnvVars ?? {}).length > 0; - - // The import API applies isSecret per call, so secret and non-secret vars go in separate calls. - let success = true; - - if (hasNonSecret) { - const result = await apiClient.importEnvVars(projectRef, environmentSlug, { - variables: envVars, - parentVariables: parentEnvVars, - override: true, - }); - success = result.success; - } - - if (hasSecret && success) { - const result = await apiClient.importEnvVars(projectRef, environmentSlug, { - variables: secretEnvVars ?? {}, - parentVariables: secretParentEnvVars, - override: true, - isSecret: true, - }); - success = result.success; - } - - return success; -} - -async function failDeploy( - client: CliApiClient, - deployment: Deployment, - error: { name: string; message: string }, - logs: string, - $spinner: ReturnType, - warnings?: string[], - errors?: string[] -) { - $spinner.stop(`Failed to deploy project`); - - const doOutputLogs = async (prefix: string = "Error") => { - if (logs.trim() !== "") { - const logPath = await saveLogs(deployment.shortCode, logs); - - printWarnings(warnings); - printErrors(errors); - - checkLogsForErrors(logs); - - outro( - `${chalkError(`${prefix}:`)} ${ - error.message - }. Full build logs have been saved to ${logPath}` - ); - } else { - outro(`${chalkError(`${prefix}:`)} ${error.message}.`); - } - }; - - const exitCommand = (message: string) => { - throw new SkipLoggingError(message); - }; - - const deploymentResponse = await client.getDeployment(deployment.id); - - if (!deploymentResponse.success) { - logger.debug(`Failed to get deployment with worker: ${deploymentResponse.error}`); - } else { - const serverDeployment = deploymentResponse.data; - - switch (serverDeployment.status) { - case "PENDING": - case "DEPLOYING": - case "BUILDING": { - await doOutputLogs(); - - await client.failDeployment(deployment.id, { - error, - }); - - exitCommand("Failed to deploy project"); - - break; - } - case "CANCELED": { - await doOutputLogs("Canceled"); - - exitCommand("Failed to deploy project"); - - break; - } - case "FAILED": { - const errorData = serverDeployment.errorData - ? prepareDeploymentError(serverDeployment.errorData) - : undefined; - - if (errorData) { - prettyError(errorData.name, errorData.stack, errorData.stderr); - - if (logs.trim() !== "") { - const logPath = await saveLogs(deployment.shortCode, logs); - - outro(`Aborting deployment. Full build logs have been saved to ${logPath}`); - } else { - outro(`Aborting deployment`); - } - } else { - await doOutputLogs("Failed"); - } - - exitCommand("Failed to deploy project"); - - break; - } - case "DEPLOYED": { - await doOutputLogs("Deployed with errors"); - - exitCommand("Deployed with errors"); - - break; - } - case "TIMED_OUT": { - await doOutputLogs("TimedOut"); - - exitCommand("Timed out"); - - break; - } - } - } -} diff --git a/packages/cli-v3/src/commands/workers/create.ts b/packages/cli-v3/src/commands/workers/create.ts deleted file mode 100644 index 683798a9cc0..00000000000 --- a/packages/cli-v3/src/commands/workers/create.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { Command } from "commander"; -import { printStandloneInitialBanner } from "../../utilities/initialBanner.js"; -import { - CommonCommandOptions, - commonOptions, - handleTelemetry, - OutroCommandError, - wrapCommandAction, -} from "../../cli/common.js"; -import { login } from "../login.js"; -import { loadConfig } from "../../config.js"; -import { resolve } from "path"; -import { getProjectClient } from "../../utilities/session.js"; -import { logger } from "../../utilities/logger.js"; -import { z } from "zod"; -import { intro, isCancel, outro, text } from "@clack/prompts"; - -const WorkersCreateCommandOptions = CommonCommandOptions.extend({ - env: z.enum(["prod", "staging"]), - config: z.string().optional(), - projectRef: z.string().optional(), -}); -type WorkersCreateCommandOptions = z.infer; - -export function configureWorkersCreateCommand(program: Command) { - return commonOptions( - program - .command("create") - .description("List all available workers") - .argument("[path]", "The path to the project", ".") - .option( - "-e, --env ", - "Deploy to a specific environment (currently only prod and staging are supported)", - "prod" - ) - .option("-c, --config ", "The name of the config file, found at [path]") - .option( - "-p, --project-ref ", - "The project ref. Required if there is no config file. This will override the project specified in the config file." - ) - .action(async (path, options) => { - await handleTelemetry(async () => { - await printStandloneInitialBanner(true, options.profile); - await workersCreateCommand(path, options); - }); - }) - ); -} - -async function workersCreateCommand(dir: string, options: unknown) { - return await wrapCommandAction( - "workerCreateCommand", - WorkersCreateCommandOptions, - options, - async (opts) => { - return await _workersCreateCommand(dir, opts); - } - ); -} - -async function _workersCreateCommand(dir: string, options: WorkersCreateCommandOptions) { - intro("Creating new worker group"); - - const authorization = await login({ - embedded: true, - defaultApiUrl: options.apiUrl, - profile: options.profile, - silent: true, - }); - - if (!authorization.ok) { - if (authorization.error === "fetch failed") { - throw new Error( - `Failed to connect to ${authorization.auth?.apiUrl}. Are you sure it's the correct URL?` - ); - } else { - throw new Error( - `You must login first. Use the \`login\` CLI command.\n\n${authorization.error}` - ); - } - } - - const projectPath = resolve(process.cwd(), dir); - - const resolvedConfig = await loadConfig({ - cwd: projectPath, - overrides: { project: options.projectRef }, - configFile: options.config, - }); - - logger.debug("Resolved config", resolvedConfig); - - const projectClient = await getProjectClient({ - accessToken: authorization.auth.accessToken, - apiUrl: authorization.auth.apiUrl, - projectRef: resolvedConfig.project, - env: options.env, - profile: options.profile, - }); - - if (!projectClient) { - throw new Error("Failed to get project client"); - } - - const name = await text({ - message: "What would you like to call the new worker?", - placeholder: "", - }); - - if (isCancel(name)) { - throw new OutroCommandError(); - } - - const description = await text({ - message: "What is the purpose of this worker?", - placeholder: "", - }); - - if (isCancel(description)) { - throw new OutroCommandError(); - } - - const newWorker = await projectClient.client.workers.create({ - name, - description, - }); - - if (!newWorker.success) { - throw new Error(`Failed to create worker: ${newWorker.error}`); - } - - outro( - `Successfully created worker ${newWorker.data.workerGroup.name} with token ${newWorker.data.token.plaintext}` - ); -} diff --git a/packages/cli-v3/src/commands/workers/index.ts b/packages/cli-v3/src/commands/workers/index.ts deleted file mode 100644 index 881a5ff899c..00000000000 --- a/packages/cli-v3/src/commands/workers/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Command } from "commander"; -import { configureWorkersBuildCommand } from "./build.js"; -import { configureWorkersListCommand } from "./list.js"; -import { configureWorkersCreateCommand } from "./create.js"; -import { configureWorkersRunCommand } from "./run.js"; - -export function configureWorkersCommand(program: Command) { - const workers = program.command("workers").description("Subcommands for managing workers"); - - configureWorkersBuildCommand(workers); - configureWorkersListCommand(workers); - configureWorkersCreateCommand(workers); - configureWorkersRunCommand(workers); - - return workers; -} diff --git a/packages/cli-v3/src/commands/workers/list.ts b/packages/cli-v3/src/commands/workers/list.ts deleted file mode 100644 index 10691c5c6da..00000000000 --- a/packages/cli-v3/src/commands/workers/list.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { Command } from "commander"; -import { printStandloneInitialBanner } from "../../utilities/initialBanner.js"; -import { - CommonCommandOptions, - commonOptions, - handleTelemetry, - wrapCommandAction, -} from "../../cli/common.js"; -import { login } from "../login.js"; -import { loadConfig } from "../../config.js"; -import { resolve } from "path"; -import { getProjectClient } from "../../utilities/session.js"; -import { logger } from "../../utilities/logger.js"; -import { z } from "zod"; -import { intro } from "@clack/prompts"; - -const WorkersListCommandOptions = CommonCommandOptions.extend({ - env: z.enum(["prod", "staging"]), - config: z.string().optional(), - projectRef: z.string().optional(), -}); -type WorkersListCommandOptions = z.infer; - -export function configureWorkersListCommand(program: Command) { - return commonOptions( - program - .command("list") - .description("List all available workers") - .argument("[path]", "The path to the project", ".") - .option( - "-e, --env ", - "Deploy to a specific environment (currently only prod and staging are supported)", - "prod" - ) - .option("-c, --config ", "The name of the config file, found at [path]") - .option( - "-p, --project-ref ", - "The project ref. Required if there is no config file. This will override the project specified in the config file." - ) - .action(async (path, options) => { - await handleTelemetry(async () => { - await printStandloneInitialBanner(true, options.profile); - await workersListCommand(path, options); - }); - }) - ); -} - -async function workersListCommand(dir: string, options: unknown) { - return await wrapCommandAction( - "workerListCommand", - WorkersListCommandOptions, - options, - async (opts) => { - return await _workersListCommand(dir, opts); - } - ); -} - -async function _workersListCommand(dir: string, options: WorkersListCommandOptions) { - intro("Listing workers"); - - const authorization = await login({ - embedded: true, - defaultApiUrl: options.apiUrl, - profile: options.profile, - silent: true, - }); - - if (!authorization.ok) { - if (authorization.error === "fetch failed") { - throw new Error( - `Failed to connect to ${authorization.auth?.apiUrl}. Are you sure it's the correct URL?` - ); - } else { - throw new Error( - `You must login first. Use the \`login\` CLI command.\n\n${authorization.error}` - ); - } - } - - const projectPath = resolve(process.cwd(), dir); - - const resolvedConfig = await loadConfig({ - cwd: projectPath, - overrides: { project: options.projectRef }, - configFile: options.config, - }); - - logger.debug("Resolved config", resolvedConfig); - - const projectClient = await getProjectClient({ - accessToken: authorization.auth.accessToken, - apiUrl: authorization.auth.apiUrl, - projectRef: resolvedConfig.project, - env: options.env, - profile: options.profile, - }); - - if (!projectClient) { - throw new Error("Failed to get project client"); - } - - const workers = await projectClient.client.workers.list(); - - if (!workers.success) { - throw new Error(`Failed to list workers: ${workers.error}`); - } - - logger.table( - workers.data.map((worker) => ({ - default: worker.isDefault ? "x" : "-", - type: worker.type, - name: worker.name, - description: worker.description ?? "-", - "updated at": worker.updatedAt.toLocaleString(), - })) - ); -} diff --git a/packages/cli-v3/src/commands/workers/run.ts b/packages/cli-v3/src/commands/workers/run.ts deleted file mode 100644 index 14427960921..00000000000 --- a/packages/cli-v3/src/commands/workers/run.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type { Command } from "commander"; -import { printStandloneInitialBanner } from "../../utilities/initialBanner.js"; -import { - CommonCommandOptions, - commonOptions, - handleTelemetry, - wrapCommandAction, -} from "../../cli/common.js"; -import { login } from "../login.js"; -import { loadConfig } from "../../config.js"; -import { resolve } from "path"; -import { getProjectClient } from "../../utilities/session.js"; -import { logger } from "../../utilities/logger.js"; -import { z } from "zod"; -import { env } from "std-env"; -import { x } from "tinyexec"; - -const WorkersRunCommandOptions = CommonCommandOptions.extend({ - env: z.enum(["prod", "staging"]), - config: z.string().optional(), - projectRef: z.string().optional(), - token: z.string().default(env.TRIGGER_WORKER_TOKEN ?? ""), - network: z.enum(["default", "none", "host"]).default("default"), -}); -type WorkersRunCommandOptions = z.infer; - -export function configureWorkersRunCommand(program: Command) { - return commonOptions( - program - .command("run") - .description("Runs a worker locally") - .argument("[path]", "The path to the project", ".") - .option( - "-e, --env ", - "Deploy to a specific environment (currently only prod and staging are supported)", - "prod" - ) - .option("-c, --config ", "The name of the config file, found at [path]") - .option( - "-p, --project-ref ", - "The project ref. Required if there is no config file. This will override the project specified in the config file." - ) - .option("-t, --token ", "The worker token to use for authentication") - .option("--network ", "The networking mode for the container", "host") - .action(async (path, options) => { - await handleTelemetry(async () => { - await printStandloneInitialBanner(true, options.profile); - await workersRunCommand(path, options); - }); - }) - ); -} - -async function workersRunCommand(dir: string, options: unknown) { - return await wrapCommandAction( - "workerRunCommand", - WorkersRunCommandOptions, - options, - async (opts) => { - return await _workersRunCommand(dir, opts); - } - ); -} - -async function _workersRunCommand(dir: string, options: WorkersRunCommandOptions) { - if (!options.token) { - throw new Error( - "You must provide a worker token to run a worker locally. Either use the `--token` flag or set the `TRIGGER_WORKER_TOKEN` environment variable." - ); - } - - logger.log("Running worker locally"); - - const authorization = await login({ - embedded: true, - defaultApiUrl: options.apiUrl, - profile: options.profile, - silent: true, - }); - - if (!authorization.ok) { - if (authorization.error === "fetch failed") { - throw new Error( - `Failed to connect to ${authorization.auth?.apiUrl}. Are you sure it's the correct URL?` - ); - } else { - throw new Error( - `You must login first. Use the \`login\` CLI command.\n\n${authorization.error}` - ); - } - } - - const projectPath = resolve(process.cwd(), dir); - - const resolvedConfig = await loadConfig({ - cwd: projectPath, - overrides: { project: options.projectRef }, - configFile: options.config, - }); - - logger.debug("Resolved config", resolvedConfig); - - const projectClient = await getProjectClient({ - accessToken: authorization.auth.accessToken, - apiUrl: authorization.auth.apiUrl, - projectRef: resolvedConfig.project, - env: options.env, - profile: options.profile, - }); - - if (!projectClient) { - throw new Error("Failed to get project client"); - } - - const deployment = await projectClient.client.deployments.unmanaged.latest(); - - if (!deployment.success) { - throw new Error("Failed to get latest deployment"); - } - - const { version, imageReference } = deployment.data; - - if (!imageReference) { - throw new Error("No image reference found for the latest deployment"); - } - - logger.log(`Version ${version}`); - logger.log(`Image: ${imageReference}`); - - const command = "docker"; - const args = [ - "run", - "--rm", - "--network", - options.network, - "-e", - `TRIGGER_WORKER_TOKEN=${options.token}`, - "-e", - `TRIGGER_API_URL=${authorization.auth.apiUrl}`, - imageReference, - ]; - - logger.debug(`Command: ${command} ${args.join(" ")}`); - logger.log(); // spacing - - const proc = x("docker", args); - - for await (const line of proc) { - logger.log(line); - } -} diff --git a/packages/cli-v3/src/consts.ts b/packages/cli-v3/src/consts.ts index 9df98d0d73e..ff1aee64010 100644 --- a/packages/cli-v3/src/consts.ts +++ b/packages/cli-v3/src/consts.ts @@ -1,4 +1,3 @@ export const COMMAND_NAME = "trigger.dev"; export const CLOUD_WEB_URL = "https://cloud.trigger.dev"; export const CLOUD_API_URL = "https://api.trigger.dev"; -export const CONFIG_FILES = ["trigger.config.ts", "trigger.config.js", "trigger.config.mjs"]; diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 0a077a5d8cb..076ef271d1e 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -153,7 +153,7 @@ export async function buildImage(options: BuildImageOptions): Promise; initializeWorker(manifest: BuildManifest, metafile: Metafile, stop: () => void): Promise; } - -export type WorkerRuntimeOptions = { - name: string | undefined; - config: ResolvedConfig; - args: DevCommandOptions; - client: CliApiClient; - dashboardUrl: string; -}; diff --git a/packages/cli-v3/src/mcp/auth.ts b/packages/cli-v3/src/mcp/auth.ts index fa5f62708ec..e0c6a7b3fcc 100644 --- a/packages/cli-v3/src/mcp/auth.ts +++ b/packages/cli-v3/src/mcp/auth.ts @@ -4,12 +4,11 @@ import { CliApiClient } from "../apiClient.js"; import { CLOUD_API_URL } from "../consts.js"; import { readAuthConfigProfile, writeAuthConfigProfile } from "../utilities/configFiles.js"; import { NotAccessTokenError, validateAccessToken } from "../utilities/accessTokens.js"; -import type { LoginResult, LoginResultOk } from "../utilities/session.js"; +import type { LoginResult } from "../utilities/session.js"; import { getPersonalAccessToken } from "../commands/login.js"; import open from "open"; import pRetry from "p-retry"; import type { McpContext } from "./context.js"; -import { ApiClient } from "@trigger.dev/core/v3"; export type McpAuthOptions = { server: McpServer; @@ -192,25 +191,3 @@ async function askForLoginPermission(server: McpServer, authorizationCodeUrl: st return result.action === "accept" && result.content?.allowLogin; } - -export async function createApiClientWithPublicJWT( - auth: LoginResultOk, - projectRef: string, - envName: string, - scopes: string[], - previewBranch?: string -) { - const cliApiClient = new CliApiClient(auth.auth.apiUrl, auth.auth.accessToken, previewBranch); - - const jwt = await cliApiClient.getJWT(projectRef, envName, { - claims: { - scopes, - }, - }); - - if (!jwt.success) { - return; - } - - return new ApiClient(auth.auth.apiUrl, jwt.data.token); -} diff --git a/packages/cli-v3/src/mcp/schemas.ts b/packages/cli-v3/src/mcp/schemas.ts index 6b49dde309c..0f8bf47f761 100644 --- a/packages/cli-v3/src/mcp/schemas.ts +++ b/packages/cli-v3/src/mcp/schemas.ts @@ -6,7 +6,7 @@ import { } from "@trigger.dev/core/v3/schemas"; import { z } from "zod"; -export const ProjectRefSchema = z +const ProjectRefSchema = z .string() .describe( "The trigger.dev project ref, starts with proj_. We will attempt to automatically detect the project ref if running inside a directory that includes a trigger.config.ts file, or if you pass the --project-ref option to the MCP server." @@ -202,7 +202,7 @@ export const ListRunsInput = CommonProjectsInput.extend({ export type ListRunsInput = z.output; -export const CommonDeployInput = CommonProjectsInput.omit({ +const CommonDeployInput = CommonProjectsInput.omit({ environment: true, }).extend({ environment: z @@ -211,7 +211,7 @@ export const CommonDeployInput = CommonProjectsInput.omit({ .default("prod"), }); -export type CommonDeployInput = z.output; +type CommonDeployInput = z.output; export const DeployInput = CommonDeployInput.extend({ skipPromotion: z diff --git a/packages/cli-v3/src/rules/install.ts b/packages/cli-v3/src/rules/install.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/cli-v3/src/types.ts b/packages/cli-v3/src/types.ts deleted file mode 100644 index 50968c9e7b6..00000000000 --- a/packages/cli-v3/src/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type TaskFile = { - triggerDir: string; - filePath: string; - importPath: string; - importName: string; -}; diff --git a/packages/cli-v3/src/utilities/analyze.ts b/packages/cli-v3/src/utilities/analyze.ts index c551d625ee7..82100314bdb 100644 --- a/packages/cli-v3/src/utilities/analyze.ts +++ b/packages/cli-v3/src/utilities/analyze.ts @@ -139,7 +139,7 @@ export function printBundleSummaryTable( console.log(table.toString()); } -export function printWarnings(workerManifest: WorkerManifest) { +function printWarnings(workerManifest: WorkerManifest) { if (!workerManifest.timings) { return; } diff --git a/packages/cli-v3/src/utilities/cliOutput.ts b/packages/cli-v3/src/utilities/cliOutput.ts index f798d36d666..9cd4f61c5b6 100644 --- a/packages/cli-v3/src/utilities/cliOutput.ts +++ b/packages/cli-v3/src/utilities/cliOutput.ts @@ -2,13 +2,11 @@ import { log } from "@clack/prompts"; import chalk from "chalk"; import type { TerminalLinkOptions } from "./terminalLink.js"; import { terminalLink } from "./terminalLink.js"; -import { hasTTY } from "std-env"; -export const isInteractive = hasTTY; export const isLinksSupported = terminalLink.isSupported; -export const green = "#4FFF54"; -export const purple = "#735BF3"; +const green = "#4FFF54"; +const purple = "#735BF3"; export function chalkGreen(text: string) { return chalk.hex(green)(text); diff --git a/packages/cli-v3/src/utilities/configFiles.ts b/packages/cli-v3/src/utilities/configFiles.ts index 4fee41d9cc4..9e29a7d1813 100644 --- a/packages/cli-v3/src/utilities/configFiles.ts +++ b/packages/cli-v3/src/utilities/configFiles.ts @@ -195,7 +195,7 @@ export function readAuthConfigFile(): CliConfigFile | null { } } -export function writeAuthConfigFile(config: CliConfigFile) { +function writeAuthConfigFile(config: CliConfigFile) { const authConfigFilePath = getAuthConfigFilePath(); mkdirSync(path.dirname(authConfigFilePath), { recursive: true, diff --git a/packages/cli-v3/src/utilities/createFileFromTemplate.ts b/packages/cli-v3/src/utilities/createFileFromTemplate.ts index 9abf291bb93..5d112fe1b5b 100644 --- a/packages/cli-v3/src/utilities/createFileFromTemplate.ts +++ b/packages/cli-v3/src/utilities/createFileFromTemplate.ts @@ -57,7 +57,7 @@ export async function createFileFromTemplate(params: { } // find strings that match ${varName} and replace with the value from a Record where { varName: "value" } -export function replaceAll(input: string, replacements: Record) { +function replaceAll(input: string, replacements: Record) { let output = input; for (const [key, value] of Object.entries(replacements)) { output = output.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value); diff --git a/packages/cli-v3/src/utilities/fileSystem.ts b/packages/cli-v3/src/utilities/fileSystem.ts index d8632d96a75..6f287fe7027 100644 --- a/packages/cli-v3/src/utilities/fileSystem.ts +++ b/packages/cli-v3/src/utilities/fileSystem.ts @@ -1,4 +1,4 @@ -import { parseJSONC, parseTOML, stringifyJSONC, stringifyTOML } from "confbox"; +import { parseJSONC, parseTOML, stringifyTOML } from "confbox"; import fsSync from "fs"; import fsModule from "fs/promises"; import stringify from "json-stable-stringify"; @@ -101,20 +101,6 @@ export async function pathExists(path: string): Promise { return fsSync.existsSync(path); } -export async function someFileExists(directory: string, filenames: string[]): Promise { - for (let index = 0; index < filenames.length; index++) { - const filename = filenames[index]; - if (!filename) continue; - - const path = pathModule.join(directory, filename); - if (await pathExists(path)) { - return true; - } - } - - return false; -} - export async function removeFile(path: string) { await fsModule.unlink(path); } @@ -191,14 +177,6 @@ export function readJSONFileSync(path: string) { return JSON.parse(fileContents); } -export function safeDeleteFileSync(path: string) { - try { - fs.unlinkSync(path); - } catch (_error) { - // ignore error - } -} - // Create a temporary directory within the OS's temp directory export async function createTempDir(): Promise { // Generate a unique temp directory path @@ -233,7 +211,3 @@ export async function safeReadJSONCFile(path: string) { return parseJSONC(fileContents.replace(/\r\n/g, "\n")); } - -export async function writeJSONCFile(path: string, json: any) { - await safeWriteFile(path, stringifyJSONC(json)); -} diff --git a/packages/cli-v3/src/utilities/getApiKeyType.ts b/packages/cli-v3/src/utilities/getApiKeyType.ts deleted file mode 100644 index 2534b47f3f4..00000000000 --- a/packages/cli-v3/src/utilities/getApiKeyType.ts +++ /dev/null @@ -1,65 +0,0 @@ -export type ApiKeyType = { - environment: "dev" | "prod"; - type: "server" | "public"; -}; - -type Result = - | { - success: true; - } - | { - success: false; - type: ApiKeyType | undefined; - }; - -export function checkApiKeyIsDevServer(apiKey: string): Result { - const type = getApiKeyType(apiKey); - - if (!type) { - return { success: false, type: undefined }; - } - - if (type.environment === "dev" && type.type === "server") { - return { - success: true, - }; - } - - return { - success: false, - type, - }; -} - -export function getApiKeyType(apiKey: string): ApiKeyType | undefined { - if (apiKey.startsWith("tr_dev_")) { - return { - environment: "dev", - type: "server", - }; - } - - if (apiKey.startsWith("pk_dev_")) { - return { - environment: "dev", - type: "public", - }; - } - - // If they enter a prod key (tr_prod_), let them know - if (apiKey.startsWith("tr_prod_")) { - return { - environment: "prod", - type: "server", - }; - } - - if (apiKey.startsWith("pk_prod_")) { - return { - environment: "prod", - type: "public", - }; - } - - return; -} diff --git a/packages/cli-v3/src/utilities/keyValueBy.ts b/packages/cli-v3/src/utilities/keyValueBy.ts deleted file mode 100644 index b14a931dd6c..00000000000 --- a/packages/cli-v3/src/utilities/keyValueBy.ts +++ /dev/null @@ -1,39 +0,0 @@ -type Index = { [key: string]: T }; -type KeyValueGenerator = (key: K, value: V, accum: Index) => Index | null; -type ArrayKeyValueGenerator = KeyValueGenerator; -type ObjectKeyValueGenerator = KeyValueGenerator; - -export function keyValueBy(arr: T[]): Index; -export function keyValueBy( - arr: T[], - keyValue: KeyValueGenerator, - initialValue?: Index -): Index; -export function keyValueBy( - obj: Index, - keyValue: KeyValueGenerator, - initialValue?: Index -): Index; - -/** Generates an object from an array or object. Simpler than reduce or _.transform. The KeyValueGenerator passes (key, value) if the input is an object, and (value, i) if it is an array. The return object from each iteration is merged into the accumulated object. Return null to skip an item. */ -export function keyValueBy( - input: T[] | Index, - // if no keyValue is given, sets all values to true - keyValue?: ArrayKeyValueGenerator | ObjectKeyValueGenerator, - accum: Index = {} -): Index { - const isArray = Array.isArray(input); - keyValue = - keyValue || ((key: T): Index => ({ [key as unknown as string]: true as unknown as R })); - // considerably faster than Array.prototype.reduce - Object.entries(input || {}).forEach(([key, value], i) => { - const o = isArray - ? (keyValue as ArrayKeyValueGenerator)(value, i, accum) - : (keyValue as ObjectKeyValueGenerator)(key, value, accum); - Object.entries(o || {}).forEach((entry) => { - accum[entry[0]] = entry[1]; - }); - }); - - return accum; -} diff --git a/packages/cli-v3/src/utilities/logger.ts b/packages/cli-v3/src/utilities/logger.ts index 64a4b3fc13e..481b20831db 100644 --- a/packages/cli-v3/src/utilities/logger.ts +++ b/packages/cli-v3/src/utilities/logger.ts @@ -4,10 +4,9 @@ import { format } from "node:util"; import chalk from "chalk"; import CLITable from "cli-table3"; import { formatMessagesSync } from "esbuild"; -import type { Message } from "esbuild"; import { env } from "std-env"; -export const LOGGER_LEVELS = { +const LOGGER_LEVELS = { none: -1, error: 0, warn: 1, @@ -16,7 +15,7 @@ export const LOGGER_LEVELS = { debug: 4, } as const; -export type LoggerLevel = keyof typeof LOGGER_LEVELS; +type LoggerLevel = keyof typeof LOGGER_LEVELS; /** A map from LOGGER_LEVEL to the error `kind` needed by `formatMessagesSync()`. */ const LOGGER_LEVEL_FORMAT_TYPE_MAP = { @@ -43,9 +42,9 @@ function getLoggerLevel(): LoggerLevel { return "log"; } -export type TableRow = Record; +type TableRow = Record; -export class Logger { +class Logger { constructor() {} loggerLevel = getLoggerLevel(); @@ -111,18 +110,3 @@ export class Logger { * to filter out logging messages. */ export const logger = new Logger(); - -export function logBuildWarnings(warnings: Message[]) { - const logs = formatMessagesSync(warnings, { kind: "warning", color: true }); - for (const log of logs) console.warn(log); -} - -/** - * Logs all errors/warnings associated with an esbuild BuildFailure in the same - * style esbuild would. - */ -export function logBuildFailure(errors: Message[], warnings: Message[]) { - const logs = formatMessagesSync(errors, { kind: "error", color: true }); - for (const log of logs) console.error(log); - logBuildWarnings(warnings); -} diff --git a/packages/cli-v3/src/utilities/obfuscateApiKey.ts b/packages/cli-v3/src/utilities/obfuscateApiKey.ts deleted file mode 100644 index 27f64d5e5a8..00000000000 --- a/packages/cli-v3/src/utilities/obfuscateApiKey.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const obfuscateApiKey = (apiKey: string) => { - const [prefix, slug, secretPart] = apiKey.split("_") as [string, string, string]; - return `${prefix}_${slug}_${"*".repeat(secretPart.length)}`; -}; diff --git a/packages/cli-v3/src/utilities/parseNameAndPath.ts b/packages/cli-v3/src/utilities/parseNameAndPath.ts deleted file mode 100644 index 4cf7d52e892..00000000000 --- a/packages/cli-v3/src/utilities/parseNameAndPath.ts +++ /dev/null @@ -1,11 +0,0 @@ -import pathModule from "node:path"; - -// Takes a relative path (like .) and resolves it to a full path (like /Users/username/Projects/my-triggers) -export const resolvePath = (input: string) => { - return pathModule.resolve(process.cwd(), input); -}; - -// Takes an absolute path and derives the relative path from the current working directory -export const relativePath = (input: string) => { - return pathModule.relative(process.cwd(), input); -}; diff --git a/packages/cli-v3/src/utilities/resolveInternalFilePath.ts b/packages/cli-v3/src/utilities/resolveInternalFilePath.ts deleted file mode 100644 index 3d790e52f83..00000000000 --- a/packages/cli-v3/src/utilities/resolveInternalFilePath.ts +++ /dev/null @@ -1,8 +0,0 @@ -import path from "path"; -import { fileURLToPath } from "url"; - -export function cliRootPath() { - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - return __dirname; -} diff --git a/packages/cli-v3/src/utilities/safeJsonParse.ts b/packages/cli-v3/src/utilities/safeJsonParse.ts deleted file mode 100644 index b7c6a6510bb..00000000000 --- a/packages/cli-v3/src/utilities/safeJsonParse.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function safeJsonParse(json?: string): unknown { - if (!json) { - return undefined; - } - - try { - return JSON.parse(json); - } catch { - return undefined; - } -} diff --git a/packages/cli-v3/src/utilities/sourceFiles.ts b/packages/cli-v3/src/utilities/sourceFiles.ts index 73eecf07432..10eea5ad723 100644 --- a/packages/cli-v3/src/utilities/sourceFiles.ts +++ b/packages/cli-v3/src/utilities/sourceFiles.ts @@ -10,7 +10,7 @@ import { join, relative } from "node:path"; import * as zlib from "node:zlib"; import { logger } from "./logger.js"; -export type FileSource = { contents: string; contentHash: string }; +type FileSource = { contents: string; contentHash: string }; export type FileSources = Record; export async function resolveFileSources( diff --git a/packages/cli-v3/src/utilities/supportsHyperlinks.ts b/packages/cli-v3/src/utilities/supportsHyperlinks.ts index 69c5ee4d31d..4b2dc350ea9 100644 --- a/packages/cli-v3/src/utilities/supportsHyperlinks.ts +++ b/packages/cli-v3/src/utilities/supportsHyperlinks.ts @@ -35,7 +35,7 @@ function parseVersion(versionString = ""): { major: number; minor: number; patch @param stream - Optional stream to check for hyperlink support. @returns boolean indicating whether hyperlinks are supported. */ -export function createSupportsHyperlinks(stream: NodeJS.WriteStream): boolean { +function createSupportsHyperlinks(stream: NodeJS.WriteStream): boolean { const { CI, CURSOR_TRACE_ID, diff --git a/packages/cli-v3/src/utilities/taskFiles.ts b/packages/cli-v3/src/utilities/taskFiles.ts deleted file mode 100644 index 728a6b4af86..00000000000 --- a/packages/cli-v3/src/utilities/taskFiles.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { ResolvedConfig } from "@trigger.dev/core/v3"; -import fs from "node:fs"; -import { join, relative, resolve } from "node:path"; -import type { TaskFile } from "../types.js"; - -export function createTaskFileImports(taskFiles: TaskFile[]) { - return taskFiles - .map( - (taskFile) => - `import * as ${taskFile.importName} from "./${taskFile.importPath}"; TaskFileImports["${ - taskFile.importName - }"] = ${taskFile.importName}; TaskFiles["${taskFile.importName}"] = ${JSON.stringify( - taskFile - )};` - ) - .join("\n"); -} - -// Find all the top-level .js or .ts files in the trigger directories -export async function gatherTaskFiles(config: ResolvedConfig): Promise> { - const taskFiles: Array = []; - - for (const triggerDir of config.triggerDirectories) { - const files = await gatherTaskFilesFromDir(triggerDir, triggerDir, config); - taskFiles.push(...files); - } - - return taskFiles; -} - -async function gatherTaskFilesFromDir( - dirPath: string, - triggerDir: string, - config: ResolvedConfig -): Promise { - const taskFiles: TaskFile[] = []; - - const files = await fs.promises.readdir(dirPath, { withFileTypes: true }); - for (const file of files) { - if (!file.isFile()) { - // Recurse into subdirectories - const fullPath = join(dirPath, file.name); - taskFiles.push(...(await gatherTaskFilesFromDir(fullPath, triggerDir, config))); - } else { - if ( - !file.name.endsWith(".js") && - !file.name.endsWith(".ts") && - !file.name.endsWith(".jsx") && - !file.name.endsWith(".tsx") - ) { - continue; - } - - const fullPath = join(dirPath, file.name); - const filePath = relative(config.projectDir, fullPath); - - //remove the file extension and replace any invalid characters with underscores - const importName = filePath.replace(/\..+$/, "").replace(/[^a-zA-Z0-9_$]/g, "_"); - - //change backslashes to forward slashes - const importPath = filePath.replace(/\\/g, "/"); - - taskFiles.push({ triggerDir, importPath, importName, filePath }); - } - } - - return taskFiles; -} - -export function resolveTriggerDirectories(projectDir: string, dirs: string[]): string[] { - return dirs.map((dir) => resolve(projectDir, dir)); -} - -const IGNORED_DIRS = ["node_modules", ".git", "dist", "build"]; - -export async function findTriggerDirectories(dirPath: string): Promise { - return getTriggerDirectories(dirPath); -} - -async function getTriggerDirectories(dirPath: string): Promise { - const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }); - const triggerDirectories: string[] = []; - - for (const entry of entries) { - if (!entry.isDirectory() || IGNORED_DIRS.includes(entry.name) || entry.name.startsWith(".")) - continue; - - const fullPath = join(dirPath, entry.name); - - // Ignore the directory if it's /app/api/trigger - if (fullPath.endsWith("app/api/trigger")) { - continue; - } - - if (entry.name === "trigger") { - triggerDirectories.push(fullPath); - } - - triggerDirectories.push(...(await getTriggerDirectories(fullPath))); - } - - return triggerDirectories; -} diff --git a/packages/cli-v3/src/utilities/windows.ts b/packages/cli-v3/src/utilities/windows.ts index 3ebf403f43b..95b72bb3651 100644 --- a/packages/cli-v3/src/utilities/windows.ts +++ b/packages/cli-v3/src/utilities/windows.ts @@ -1,11 +1,7 @@ import { log, spinner as clackSpinner } from "@clack/prompts"; import { isWindows as stdEnvIsWindows } from "std-env"; -export const isWindows = stdEnvIsWindows; - -export function escapeImportPath(path: string) { - return isWindows ? path.replaceAll("\\", "\\\\") : path; -} +const isWindows = stdEnvIsWindows; // Removes ANSI escape sequences to get actual visible length function getVisibleLength(str: string): number { diff --git a/packages/core/package.json b/packages/core/package.json index 8ce700a5c4f..e957be0fd33 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -214,12 +214,10 @@ "@opentelemetry/sdk-metrics": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1", "@opentelemetry/sdk-trace-node": "2.7.1", - "@opentelemetry/semantic-conventions": "1.41.1", "@s2-dev/streamstore": "0.25.0", "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.18", @@ -229,18 +227,14 @@ "tinyexec": "^0.3.2", "uncrypto": "^0.1.3", "zod": "3.25.76", - "zod-error": "1.5.0", "zod-validation-error": "^1.5.0" }, "devDependencies": { - "@ai-sdk/provider-utils": "^1.0.22", "@arethetypeswrong/cli": "^0.18.5", "@epic-web/test-server": "^0.1.0", "@internal/testcontainers": "workspace:*", "@trigger.dev/database": "workspace:*", "@types/humanize-duration": "^3.27.1", - "@types/lodash.get": "^4.4.9", - "@types/readable-stream": "^4.0.14", "ai": "^6.0.0", "ai-v7": "npm:ai@7.0.0-canary.159", "defu": "^6.1.4", diff --git a/packages/core/src/debounce.ts b/packages/core/src/debounce.ts deleted file mode 100644 index 130bfc17acd..00000000000 --- a/packages/core/src/debounce.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** A very simple debounce. Will only execute after the specified delay has elapsed since the last call. */ -export function debounce( - func: (...args: any[]) => void, - delayMs: number -): (...args: any[]) => void { - let timeoutId: NodeJS.Timeout | null = null; - - return (...args: any[]) => { - // Clear any existing timeout - if (timeoutId) { - clearTimeout(timeoutId); - } - - // Set a new timeout with the latest args - timeoutId = setTimeout(() => { - func(...args); - timeoutId = null; - }, delayMs); - }; -} diff --git a/packages/core/src/v3/apiClient/runStream.ts b/packages/core/src/v3/apiClient/runStream.ts index b0d43ef3f99..ffd9bb18084 100644 --- a/packages/core/src/v3/apiClient/runStream.ts +++ b/packages/core/src/v3/apiClient/runStream.ts @@ -89,7 +89,7 @@ export type RunShapeStreamOptions = { onFetchError?: (e: Error) => void; }; -export type StreamPartResult> = { +type StreamPartResult> = { [K in keyof TStreams]: { type: K; chunk: TStreams[K]; @@ -665,10 +665,6 @@ export class SSEStreamSubscriptionFactory implements StreamSubscriptionFactory { } } -export interface RunShapeProvider { - onShape(callback: (shape: SubscribeRunRawShape) => Promise): Promise<() => void>; -} - export type RunSubscriptionOptions = RunShapeStreamOptions & { runShapeStream: ReadableStream; stopRunShapeStream: () => void; diff --git a/packages/core/src/v3/apiClient/stream.ts b/packages/core/src/v3/apiClient/stream.ts index ec35f1deb63..9f9725c9353 100644 --- a/packages/core/src/v3/apiClient/stream.ts +++ b/packages/core/src/v3/apiClient/stream.ts @@ -224,37 +224,3 @@ class ReadableShapeStream = Row> { this.#unsubscribe?.(); } } - -export class LineTransformStream extends TransformStream { - private buffer = ""; - - constructor() { - super({ - transform: (chunk, controller) => { - // Append the chunk to the buffer - this.buffer += chunk; - - // Split on newlines - const lines = this.buffer.split("\n"); - - // The last element might be incomplete, hold it back in buffer - this.buffer = lines.pop() || ""; - - // Filter out empty or whitespace-only lines - const fullLines = lines.filter((line) => line.trim().length > 0); - - // If we got any complete lines, emit them as an array - if (fullLines.length > 0) { - controller.enqueue(fullLines); - } - }, - flush: (controller) => { - // On stream end, if there's leftover text, emit it as a single-element array - const trimmed = this.buffer.trim(); - if (trimmed.length > 0) { - controller.enqueue([trimmed]); - } - }, - }); - } -} diff --git a/packages/core/src/v3/apiClientManager/index.ts b/packages/core/src/v3/apiClientManager/index.ts index cd52af6abc1..cf9f9348914 100644 --- a/packages/core/src/v3/apiClientManager/index.ts +++ b/packages/core/src/v3/apiClientManager/index.ts @@ -17,7 +17,7 @@ function getDevBranchEnvVar(): string | undefined { return value && !isDefaultDevBranch(value) ? value : undefined; } -export class ApiClientMissingError extends Error { +class ApiClientMissingError extends Error { constructor(message: string) { super(message); this.name = "ApiClientMissingError"; diff --git a/packages/core/src/v3/clock/preciseWallClock.ts b/packages/core/src/v3/clock/preciseWallClock.ts index 94dc4ce5c60..95ebdbb866a 100644 --- a/packages/core/src/v3/clock/preciseWallClock.ts +++ b/packages/core/src/v3/clock/preciseWallClock.ts @@ -1,7 +1,7 @@ import { PreciseDate } from "@google-cloud/precise-date"; import type { Clock, ClockTime } from "./clock.js"; -export type PreciseWallClockOptions = { +type PreciseWallClockOptions = { origin?: ClockTime; now?: PreciseDate; }; diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index 9672b6fec62..b3f1657a633 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -33,7 +33,7 @@ export type OnStartHookFunction; -export type TaskStartAttemptHookParams = { +type TaskStartAttemptHookParams = { ctx: TaskRunContext; payload: TPayload; task: string; @@ -142,12 +142,12 @@ export type OnSuccessHookFunction< export type AnyOnSuccessHookFunction = OnSuccessHookFunction; -export type TaskCompleteSuccessResult = { +type TaskCompleteSuccessResult = { ok: true; data: TOutput; }; -export type TaskCompleteErrorResult = { +type TaskCompleteErrorResult = { ok: false; error: unknown; }; diff --git a/packages/core/src/v3/logger/taskLogger.ts b/packages/core/src/v3/logger/taskLogger.ts index 363717defd4..4fc3ba0d2ef 100644 --- a/packages/core/src/v3/logger/taskLogger.ts +++ b/packages/core/src/v3/logger/taskLogger.ts @@ -13,7 +13,7 @@ export type LogLevel = "none" | "error" | "warn" | "info" | "debug" | "log"; export const logLevels: Array = ["none", "error", "warn", "info", "debug"]; -export type TaskLoggerConfig = { +type TaskLoggerConfig = { logger: Logger; tracer: TriggerTracer; level: LogLevel; diff --git a/packages/core/src/v3/otel/tracingSDK.ts b/packages/core/src/v3/otel/tracingSDK.ts index 0f4ea82a227..9f8de5b6676 100644 --- a/packages/core/src/v3/otel/tracingSDK.ts +++ b/packages/core/src/v3/otel/tracingSDK.ts @@ -656,7 +656,7 @@ function isValidAndNotEmpty(name: string | undefined): boolean { return isValid(name) && name.length > 0; } -export function parseOtelResourceAttributes( +function parseOtelResourceAttributes( rawEnvAttributes: string | undefined | null ): Record { if (!rawEnvAttributes) return {}; diff --git a/packages/core/src/v3/realtimeStreams/index.ts b/packages/core/src/v3/realtimeStreams/index.ts index e9d80ef51a7..07c0737142f 100644 --- a/packages/core/src/v3/realtimeStreams/index.ts +++ b/packages/core/src/v3/realtimeStreams/index.ts @@ -9,18 +9,6 @@ import type { // Re-export the session-scoped stream instance so the SDK's // `SessionOutputChannel.pipe` / `.writer` can construct it without reaching // into the core package's internals. -export { SessionStreamInstance } from "./sessionStreamInstance.js"; -export type { - SessionStreamInstanceOptions, - InitializeSessionStreamResponseLike, -} from "./sessionStreamInstance.js"; -export { - trimSessionStream, - writeSessionControlRecord, - writeTurnCompleteRecord, - writeUpgradeRequiredRecord, -} from "./sessionStreamOneshot.js"; - const API_NAME = "realtime-streams"; const NOOP_MANAGER = new NoopRealtimeStreamsManager(); diff --git a/packages/core/src/v3/runEngineWorker/supervisor/events.ts b/packages/core/src/v3/runEngineWorker/supervisor/events.ts index a537ed137a1..e036705cdca 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/events.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/events.ts @@ -49,5 +49,3 @@ export type WorkerEvents = { }, ]; }; - -export type WorkerEventArgs = WorkerEvents[T]; diff --git a/packages/core/src/v3/runEngineWorker/supervisor/util.ts b/packages/core/src/v3/runEngineWorker/supervisor/util.ts index 94386016ffb..1ea06ec9783 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/util.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/util.ts @@ -12,29 +12,3 @@ export function getDefaultWorkerHeaders( [WORKER_HEADERS.MANAGED_SECRET]: options.managedWorkerSecret, }); } - -function redactString(value: string, end = 10) { - return value.slice(0, end) + "*".repeat(value.length - end); -} - -function redactNumber(value: number, end = 10) { - const str = String(value); - const redacted = redactString(str, end); - return Number(redacted); -} - -export function redactKeys>(obj: T, keys: Array): T { - const redacted = { ...obj }; - for (const key of keys) { - const value = obj[key]; - - if (typeof value === "number") { - redacted[key] = redactNumber(value) as any; - } else if (typeof value === "string") { - redacted[key] = redactString(value) as any; - } else { - continue; - } - } - return redacted; -} diff --git a/packages/core/src/v3/test/mock-task-context.ts b/packages/core/src/v3/test/mock-task-context.ts index 5fbe1957613..085acf999f1 100644 --- a/packages/core/src/v3/test/mock-task-context.ts +++ b/packages/core/src/v3/test/mock-task-context.ts @@ -24,7 +24,7 @@ import { TestSessionStreamManager } from "./test-session-stream-manager.js"; * `TaskRunContext`. Each sub-object is a partial of its real shape — * unset fields get sensible defaults. */ -export type MockTaskRunContextOverrides = { +type MockTaskRunContextOverrides = { task?: Partial; attempt?: Partial; run?: Partial; diff --git a/packages/core/src/v3/types/schemas.ts b/packages/core/src/v3/types/schemas.ts index b4121029925..9dc66d9eaca 100644 --- a/packages/core/src/v3/types/schemas.ts +++ b/packages/core/src/v3/types/schemas.ts @@ -1,4 +1,4 @@ -export type SchemaZodEsque = { +type SchemaZodEsque = { _input: TInput; _output: TParsedInput; }; @@ -15,7 +15,7 @@ export function isSchemaZodEsque( ); } -export type SchemaValibotEsque = { +type SchemaValibotEsque = { schema: { _types?: { input: TInput; @@ -30,7 +30,7 @@ export function isSchemaValibotEsque( return typeof schema === "object" && "_types" in schema; } -export type SchemaArkTypeEsque = { +type SchemaArkTypeEsque = { inferIn: TInput; infer: TParsedInput; }; @@ -41,39 +41,39 @@ export function isSchemaArkTypeEsque( return typeof schema === "object" && "_inferIn" in schema && "_infer" in schema; } -export type SchemaMyZodEsque = { +type SchemaMyZodEsque = { parse: (input: any) => TInput; }; -export type SchemaSuperstructEsque = { +type SchemaSuperstructEsque = { create: (input: unknown) => TInput; }; -export type SchemaCustomValidatorEsque = (input: unknown) => Promise | TInput; +type SchemaCustomValidatorEsque = (input: unknown) => Promise | TInput; -export type SchemaYupEsque = { +type SchemaYupEsque = { validateSync: (input: unknown) => TInput; }; -export type SchemaScaleEsque = { +type SchemaScaleEsque = { assert(value: unknown): asserts value is TInput; }; -export type SchemaWithoutInput = +type SchemaWithoutInput = | SchemaCustomValidatorEsque | SchemaMyZodEsque | SchemaScaleEsque | SchemaSuperstructEsque | SchemaYupEsque; -export type SchemaWithInputOutput = +type SchemaWithInputOutput = | SchemaZodEsque | SchemaValibotEsque | SchemaArkTypeEsque; export type Schema = SchemaWithInputOutput | SchemaWithoutInput; -export type inferSchema = +type inferSchema = TSchema extends SchemaWithInputOutput ? { in: $TIn; diff --git a/packages/core/src/v3/usage/usageClient.ts b/packages/core/src/v3/usage/usageClient.ts index 374481c3760..d4a7fcbec45 100644 --- a/packages/core/src/v3/usage/usageClient.ts +++ b/packages/core/src/v3/usage/usageClient.ts @@ -1,10 +1,5 @@ import { apiClientManager } from "../apiClientManager-api.js"; -export type UsageClientOptions = { - token: string; - baseUrl: string; -}; - export type UsageEvent = { durationMs: number; }; diff --git a/packages/core/src/v3/utils/safeAsyncLocalStorage.ts b/packages/core/src/v3/utils/safeAsyncLocalStorage.ts deleted file mode 100644 index 60c01dcca58..00000000000 --- a/packages/core/src/v3/utils/safeAsyncLocalStorage.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { AsyncLocalStorage } from "node:async_hooks"; - -export class SafeAsyncLocalStorage { - private storage: AsyncLocalStorage; - - constructor() { - this.storage = new AsyncLocalStorage(); - } - - enterWith(context: T): void { - this.storage.enterWith(context); - } - - runWith Promise>(context: T, fn: R): Promise> { - return this.storage.run(context, fn); - } - - getStore(): T | undefined { - return this.storage.getStore(); - } -} diff --git a/packages/plugins/src/rbac.ts b/packages/plugins/src/rbac.ts index e32187e215b..6cc4e2b43c4 100644 --- a/packages/plugins/src/rbac.ts +++ b/packages/plugins/src/rbac.ts @@ -621,7 +621,7 @@ export type RoleMutationResult = { ok: true; role: Role } | { ok: false; error: // `code` is an optional machine-readable reason so callers can branch on // expected outcomes (e.g. `last_owner`, the guard that keeps an org from // losing its final Owner) instead of matching the free-text `error`. -export type RoleAssignmentErrorCode = "last_owner"; +type RoleAssignmentErrorCode = "last_owner"; export type RoleAssignmentResult = | { ok: true } | { ok: false; error: string; code?: RoleAssignmentErrorCode }; diff --git a/packages/python/package.json b/packages/python/package.json index 89660957631..67e6927ad90 100644 --- a/packages/python/package.json +++ b/packages/python/package.json @@ -54,7 +54,6 @@ "tshy": "^4.1.3", "typescript": "catalog:", "tsx": "4.17.0", - "esbuild": "^0.23.0", "@arethetypeswrong/cli": "^0.18.5", "@trigger.dev/build": "workspace:4.5.11", "@trigger.dev/sdk": "workspace:4.5.11" diff --git a/packages/react-hooks/src/utils/createContextAndHook.ts b/packages/react-hooks/src/utils/createContextAndHook.ts index c48bddd54a5..5729f01730f 100644 --- a/packages/react-hooks/src/utils/createContextAndHook.ts +++ b/packages/react-hooks/src/utils/createContextAndHook.ts @@ -1,7 +1,7 @@ "use client"; import React from "react"; -export function assertContextExists( +function assertContextExists( contextVal: unknown, msgOrCtx: string | React.Context ): asserts contextVal { diff --git a/packages/react-hooks/src/utils/trigger-swr.ts b/packages/react-hooks/src/utils/trigger-swr.ts index 77fa8b83573..1a5089a7cef 100644 --- a/packages/react-hooks/src/utils/trigger-swr.ts +++ b/packages/react-hooks/src/utils/trigger-swr.ts @@ -5,7 +5,7 @@ import type { ApiRequestOptions } from "@trigger.dev/core/v3"; // eslint-disable-next-line import/export export * from "swr"; // eslint-disable-next-line import/export -export { default as useSWR, SWRConfig } from "swr"; +export { default as useSWR } from "swr"; export type CommonTriggerHookOptions = { /** diff --git a/packages/redis-worker/package.json b/packages/redis-worker/package.json index d185e72281c..ae457fa4cac 100644 --- a/packages/redis-worker/package.json +++ b/packages/redis-worker/package.json @@ -24,7 +24,6 @@ }, "dependencies": { "@trigger.dev/core": "workspace:4.5.11", - "lodash.omit": "^4.5.0", "nanoid": "^5.1.16", "p-limit": "^6.2.0", "seedrandom": "^3.0.5", @@ -35,7 +34,6 @@ "@internal/redis": "workspace:*", "@internal/testcontainers": "workspace:*", "@internal/tracing": "workspace:*", - "@types/lodash.omit": "^4.5.7", "@types/seedrandom": "^3.0.8", "esbuild": "^0.23.0", "rimraf": "6.0.1", diff --git a/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts b/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts index 4e7d740d4bd..8e041232c3f 100644 --- a/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts +++ b/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts @@ -7,7 +7,7 @@ import type { QueueWithScore, } from "../types.js"; -export interface RoundRobinSchedulerConfig { +interface RoundRobinSchedulerConfig { redis: RedisOptions; keys: FairQueueKeyProducer; /** Maximum queues to fetch from master queue per iteration */ diff --git a/packages/rsc/package.json b/packages/rsc/package.json index 3ce769ed6e8..2009c5f80c1 100644 --- a/packages/rsc/package.json +++ b/packages/rsc/package.json @@ -44,10 +44,7 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.5", - "@trigger.dev/build": "workspace:^4.5.11", "@types/node": "^24.13.3", - "@types/react": "*", - "@types/react-dom": "*", "rimraf": "^6.0.1", "tshy": "^4.1.3", "tsx": "4.17.0" diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index 05c709843c3..42c487c13d9 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -78,29 +78,17 @@ "@opentelemetry/api": "1.9.1", "@opentelemetry/semantic-conventions": "1.41.1", "@trigger.dev/core": "workspace:4.5.11", - "chalk": "^5.2.0", - "cronstrue": "^2.21.0", - "debug": "^4.3.4", - "evt": "^2.4.13", - "slug": "^6.0.0", - "ulid": "^2.3.0", - "uncrypto": "^0.1.3", - "ws": "^8.11.0" + "uncrypto": "^0.1.3" }, "devDependencies": { "@ai-sdk/provider": "3.0.8", "@arethetypeswrong/cli": "^0.18.5", - "@types/debug": "^4.1.7", "@types/react": "^19.2.14", - "@types/slug": "^5.0.3", - "@types/ws": "^8.5.3", "ai": "^6.0.116", "ai-v7": "npm:ai@7.0.0-canary.159", - "encoding": "^0.1.13", "rimraf": "^6.0.1", "tshy": "^4.1.3", "tsx": "4.17.0", - "typed-emitter": "^2.1.0", "typescript": "catalog:", "zod": "3.25.76" }, diff --git a/packages/trigger-sdk/src/v3/auth.ts b/packages/trigger-sdk/src/v3/auth.ts index d26a08fa874..ad29158e32f 100644 --- a/packages/trigger-sdk/src/v3/auth.ts +++ b/packages/trigger-sdk/src/v3/auth.ts @@ -83,7 +83,7 @@ type PublicTokenPermissionProperties = { sessions?: string | string[]; }; -export type PublicTokenPermissions = { +type PublicTokenPermissions = { read?: PublicTokenPermissionProperties; write?: PublicTokenPermissionProperties; @@ -103,7 +103,7 @@ export type PublicTokenPermissions = { }; }; -export type CreatePublicTokenOptions = { +type CreatePublicTokenOptions = { /** * A collection of permission scopes to be granted to the token. This remains * optional for root API key compatibility; additional API keys require at @@ -251,7 +251,7 @@ async function withPublicToken(options: CreatePublicTokenOptions, fn: () => Prom await withAuth({ accessToken: token }, fn); } -export type CreateTriggerTokenOptions = { +type CreateTriggerTokenOptions = { /** * The expiration time for the token: a duration string, a `Date`, or a Unix * timestamp in **seconds**. diff --git a/packages/trigger-sdk/src/v3/chat-client.ts b/packages/trigger-sdk/src/v3/chat-client.ts index f632f5e89d5..919d855e5e0 100644 --- a/packages/trigger-sdk/src/v3/chat-client.ts +++ b/packages/trigger-sdk/src/v3/chat-client.ts @@ -58,16 +58,16 @@ export type ChatSession = { * `AgentChat`. Same shape as the type on `TriggerChatTransport` — these * mirror so customers can share a single resolver between the two clients. */ -export type AgentChatEndpoint = "in" | "out"; +type AgentChatEndpoint = "in" | "out"; -export type AgentChatEndpointContext = { +type AgentChatEndpointContext = { endpoint: AgentChatEndpoint; chatId: string; }; -export type AgentChatBaseURLResolver = (ctx: AgentChatEndpointContext) => string; +type AgentChatBaseURLResolver = (ctx: AgentChatEndpointContext) => string; -export type AgentChatFetchOverride = ( +type AgentChatFetchOverride = ( url: string, init: RequestInit, ctx: AgentChatEndpointContext diff --git a/packages/trigger-sdk/src/v3/retry.ts b/packages/trigger-sdk/src/v3/retry.ts index 1da657b61e7..110d8d857b6 100644 --- a/packages/trigger-sdk/src/v3/retry.ts +++ b/packages/trigger-sdk/src/v3/retry.ts @@ -116,7 +116,7 @@ function onThrow( ); } -export interface RetryFetchRequestInit extends RequestInit { +interface RetryFetchRequestInit extends RequestInit { retry?: FetchRetryOptions; timeoutInMs?: number; } diff --git a/packages/trigger-sdk/src/v3/runs.ts b/packages/trigger-sdk/src/v3/runs.ts index 3bd2a9ea7f8..9f5eb934c36 100644 --- a/packages/trigger-sdk/src/v3/runs.ts +++ b/packages/trigger-sdk/src/v3/runs.ts @@ -70,7 +70,6 @@ export const runs = { fetchStream, }; -export type ListRunsItem = ListRunResponseItem; export type BulkAction = BulkActionObject; function listRuns( @@ -457,8 +456,6 @@ function rescheduleRun( return apiClient.rescheduleRun(runId, body, $requestOptions); } -export type PollOptions = { pollIntervalMs?: number }; - const MAX_POLL_ATTEMPTS = 500; async function poll( @@ -485,7 +482,7 @@ async function poll( ); } -export type SubscribeToRunOptions = { +type SubscribeToRunOptions = { /** * Whether to close the subscription when the run completes * @@ -563,7 +560,7 @@ function subscribeToRun( }); } -export type SubscribeToRunsFilterOptions = { +type SubscribeToRunsFilterOptions = { /** * Filter runs by the time they were created. You must specify the duration string like "1h", "10s", "30m", etc. * diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index d06e82ae1bd..13125b305ca 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -1,5 +1,4 @@ import { SpanKind } from "@opentelemetry/api"; -import type { SerializableJson } from "@trigger.dev/core"; import { type ApiClient, type ApiRequestOptions, @@ -71,17 +70,13 @@ import { type inferToolParameters, type RunHandle, type RunHandleFromTypes, - type RunHandleOutput, - type RunHandlePayload, type RunTypes, type SchemaParseFn, type Task, - type TaskBatchOutputHandle, type TaskIdentifier, type TaskOptions, type TaskOptionsWithSchema, type TaskOutput, - type TaskOutputHandle, type TaskPayload, type TaskRunResult, type TaskSchema, @@ -106,17 +101,12 @@ export type { BatchTriggerOptions, Queue, RunHandle, - RunHandleOutput, - RunHandlePayload, - SerializableJson, Task, - TaskBatchOutputHandle, TaskFromIdentifier, TaskIdentifier, TaskOptions, TaskOptionsWithSchema, TaskOutput, - TaskOutputHandle, TaskPayload, TaskRunResult, TaskSchema, diff --git a/packages/trigger-sdk/src/v3/test/mock-chat-agent.ts b/packages/trigger-sdk/src/v3/test/mock-chat-agent.ts index a4cbe7b33a0..a669975f383 100644 --- a/packages/trigger-sdk/src/v3/test/mock-chat-agent.ts +++ b/packages/trigger-sdk/src/v3/test/mock-chat-agent.ts @@ -13,9 +13,7 @@ import { import { createTestSessionHandle, type TestSessionOutState } from "./test-session-handle.js"; /** Pre-seed locals before the agent's `run()` starts. */ -export type SetupLocals = (locals: { - set(key: LocalsKey, value: T): void; -}) => void | Promise; +type SetupLocals = (locals: { set(key: LocalsKey, value: T): void }) => void | Promise; // The slim wire payload shape used by chat.agent tasks. Kept loose here so we // don't import from the backend-only ai.ts module. At most ONE message per diff --git a/packages/trigger-sdk/src/v3/test/test-session-handle.ts b/packages/trigger-sdk/src/v3/test/test-session-handle.ts index 860b7694e6e..945cd231152 100644 --- a/packages/trigger-sdk/src/v3/test/test-session-handle.ts +++ b/packages/trigger-sdk/src/v3/test/test-session-handle.ts @@ -106,7 +106,7 @@ async function drainInto( * Mirrors {@link SessionOutputChannel}'s public shape — `pipe` / `writer` * / `append` / `read` — so the agent's existing code paths work unchanged. */ -export class TestSessionOutputChannel extends SessionOutputChannel { +class TestSessionOutputChannel extends SessionOutputChannel { constructor( sessionId: string, private readonly state: TestSessionOutState diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d6eb1b1d54..3a65d42dbb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,13 +111,7 @@ importers: agentcrumbs: specifier: ^0.5.0 version: 0.5.0 - node-fetch: - specifier: 2.6.x - version: 2.6.7(encoding@0.1.13) devDependencies: - '@manypkg/cli': - specifier: ^0.19.2 - version: 0.19.2 '@playwright/test': specifier: ^1.36.2 version: 1.37.0 @@ -130,9 +124,6 @@ importers: '@vitest/coverage-v8': specifier: 4.1.7 version: 4.1.7(vitest@4.1.7) - autoprefixer: - specifier: ^10.4.12 - version: 10.4.13(postcss@8.5.26) knip: specifier: 6.25.0 version: 6.25.0 @@ -160,9 +151,6 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 - vite-tsconfig-paths: - specifier: ^4.0.5 - version: 4.0.5(typescript@7.0.2) vitest: specifier: 4.1.7 version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.7)(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@3.12.2)(yaml@2.9.0)) @@ -623,12 +611,6 @@ importers: json-stable-stringify: specifier: ^1.3.0 version: 1.3.0 - jsonpointer: - specifier: ^5.0.1 - version: 5.0.1 - lodash.omit: - specifier: ^4.5.0 - version: 4.5.0 lru-cache: specifier: ^11.2.4 version: 11.2.4 @@ -689,9 +671,6 @@ importers: prom-client: specifier: ^15.1.0 version: 15.1.0 - prop-types: - specifier: ^15.8.1 - version: 15.8.1 qrcode.react: specifier: ^4.2.0 version: 4.2.0(react@18.3.1) @@ -825,18 +804,9 @@ importers: '@remix-run/dev': specifier: 2.17.5 version: 2.17.5(@remix-run/react@2.17.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@7.0.2))(@remix-run/serve@2.17.5(typescript@7.0.2))(@types/node@24.13.3)(bufferutil@4.0.9)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(typescript@7.0.2)(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0))(yaml@2.9.0) - '@remix-run/testing': - specifier: ^2.17.5 - version: 2.17.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@7.0.2) '@sentry/cli': specifier: 2.50.2 version: 2.50.2(encoding@0.1.13) - '@swc/core': - specifier: ^1.3.4 - version: 1.3.26 - '@swc/helpers': - specifier: ^0.4.11 - version: 0.4.14 '@tailwindcss/forms': specifier: ^0.5.11 version: 0.5.11(tailwindcss@4.3.1) @@ -852,9 +822,6 @@ importers: '@total-typescript/ts-reset': specifier: ^0.4.2 version: 0.4.2 - '@types/bcryptjs': - specifier: ^2.4.2 - version: 2.4.2 '@types/compression': specifier: ^1.7.2 version: 1.7.2 @@ -864,27 +831,18 @@ importers: '@types/express': specifier: ^4.17.13 version: 4.17.15 - '@types/json-query': - specifier: ^2.2.3 - version: 2.2.3 '@types/marked': specifier: ^4.0.3 version: 4.0.8 '@types/morgan': specifier: ^1.9.3 version: 1.9.4 - '@types/node-fetch': - specifier: ^2.6.2 - version: 2.6.2 '@types/pg': specifier: ^8.11.10 version: 8.11.14 '@types/prismjs': specifier: ^1.26.0 version: 1.26.0 - '@types/qs': - specifier: ^6.9.7 - version: 6.9.7 '@types/react': specifier: 18.2.69 version: 18.2.69 @@ -903,21 +861,12 @@ importers: '@types/supertest': specifier: ^6.0.2 version: 6.0.2 - '@types/tar': - specifier: ^6.1.4 - version: 6.1.4 '@types/ws': specifier: ^8.5.3 version: 8.5.4 autoevals: specifier: ^0.0.130 version: 0.0.130(encoding@0.1.13)(ws@8.21.0(bufferutil@4.0.9)) - css-loader: - specifier: ^6.10.0 - version: 6.10.0(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)) - datepicker: - specifier: link:@types/@react-aria/datepicker - version: link:@types/@react-aria/datepicker engine.io: specifier: ^6.6.7 version: 6.6.8(bufferutil@4.0.9) @@ -927,27 +876,12 @@ importers: evalite: specifier: 1.0.0-beta.16 version: 1.0.0-beta.16(ai@6.0.116(zod@3.25.76))(better-sqlite3@11.10.0)(bufferutil@4.0.9) - postcss-import: - specifier: ^16.0.1 - version: 16.0.1(postcss@8.5.26) - postcss-loader: - specifier: ^8.1.1 - version: 8.1.1(postcss@8.5.26)(typescript@7.0.2)(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)) - rimraf: - specifier: ^6.0.1 - version: 6.0.1 - style-loader: - specifier: ^3.3.4 - version: 3.3.4(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)) supertest: specifier: ^7.0.0 version: 7.0.0 tailwind-scrollbar: specifier: ^4.0.2 version: 4.0.2(react@18.3.1)(tailwindcss@4.3.1) - tsconfig-paths: - specifier: ^3.14.1 - version: 3.14.1 tsx: specifier: ^4.20.6 version: 4.20.6 @@ -971,9 +905,6 @@ importers: '@internal/redis': specifier: workspace:* version: link:../redis - '@trigger.dev/core': - specifier: workspace:* - version: link:../../packages/core '@unkey/cache': specifier: ^1.5.0 version: 1.5.0 @@ -1103,9 +1034,6 @@ importers: specifier: 6.14.0 version: 6.14.0(magicast@0.3.5)(typescript@7.0.2) devDependencies: - '@types/decimal.js': - specifier: ^7.4.3 - version: 7.4.3 rimraf: specifier: 6.0.1 version: 6.0.1 @@ -1136,9 +1064,6 @@ importers: resend: specifier: ^3.2.0 version: 3.2.0 - tiny-invariant: - specifier: ^1.2.0 - version: 1.3.1 zod: specifier: 3.25.76 version: 3.25.76 @@ -1225,9 +1150,6 @@ importers: '@types/node': specifier: 24.13.3 version: 24.13.3 - rimraf: - specifier: ^6.0.1 - version: 6.0.1 ts-proto: specifier: ^1.167.3 version: 1.167.3 @@ -1364,9 +1286,6 @@ importers: internal-packages/run-store: dependencies: - '@internal/run-ops-database': - specifier: workspace:* - version: link:../run-ops-database '@trigger.dev/core': specifier: workspace:* version: link:../../packages/core @@ -1374,6 +1293,9 @@ importers: specifier: workspace:* version: link:../database devDependencies: + '@internal/run-ops-database': + specifier: workspace:* + version: link:../run-ops-database '@internal/testcontainers': specifier: workspace:* version: link:../testcontainers @@ -1401,9 +1323,6 @@ importers: cron-parser: specifier: ^4.9.0 version: 4.9.0 - cronstrue: - specifier: ^2.50.0 - version: 2.61.0 zod: specifier: 3.25.76 version: 3.25.76 @@ -1416,11 +1335,10 @@ importers: version: 6.0.1 internal-packages/sdk-compat-tests: - dependencies: + devDependencies: '@trigger.dev/sdk': specifier: workspace:* version: link:../../packages/trigger-sdk - devDependencies: esbuild: specifier: ^0.24.0 version: 0.24.2 @@ -1436,9 +1354,6 @@ importers: internal-packages/sso: dependencies: - '@trigger.dev/core': - specifier: workspace:* - version: link:../../packages/core '@trigger.dev/plugins': specifier: workspace:* version: link:../../packages/plugins @@ -1461,9 +1376,6 @@ importers: '@clickhouse/client': specifier: ^1.11.1 version: 1.11.1 - '@opentelemetry/api': - specifier: ^1.9.1 - version: 1.9.1 '@trigger.dev/database': specifier: workspace:* version: link:../database @@ -1507,15 +1419,9 @@ importers: internal-packages/tsql: dependencies: - '@trigger.dev/core': - specifier: workspace:* - version: link:../../packages/core antlr4ts: specifier: 0.5.0-alpha.4 version: 0.5.0-alpha.4(patch_hash=b5d41129ddbf7c4cbb0288244b2c8d041ee0803c5102c1181c180408d3b579f4) - zod: - specifier: 3.25.76 - version: 3.25.76 devDependencies: antlr4ts-cli: specifier: 0.5.0-alpha.4 @@ -1576,9 +1482,6 @@ importers: pkg-types: specifier: ^1.1.3 version: 1.1.3 - resolve: - specifier: ^1.22.8 - version: 1.22.8 tinyglobby: specifier: ^0.2.2 version: 0.2.2 @@ -1589,15 +1492,9 @@ importers: '@arethetypeswrong/cli': specifier: ^0.18.5 version: 0.18.5 - '@types/resolve': - specifier: ^1.20.6 - version: 1.20.6 '@typescript/typescript6': specifier: 6.0.2 version: 6.0.2 - esbuild: - specifier: ^0.23.0 - version: 0.23.0 rimraf: specifier: 6.0.1 version: 6.0.1 @@ -1634,24 +1531,9 @@ importers: '@opentelemetry/api-logs': specifier: 0.218.0 version: 0.218.0 - '@opentelemetry/exporter-trace-otlp-http': - specifier: 0.218.0 - version: 0.218.0(@opentelemetry/api@1.9.1) '@opentelemetry/instrumentation': specifier: 0.218.0 version: 0.218.0(@opentelemetry/api@1.9.1)(supports-color@10.0.0) - '@opentelemetry/instrumentation-fetch': - specifier: 0.218.0 - version: 0.218.0(@opentelemetry/api@1.9.1)(supports-color@10.0.0) - '@opentelemetry/resources': - specifier: 2.7.1 - version: 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-node': - specifier: 2.7.1 - version: 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': - specifier: 1.41.1 - version: 1.41.1 '@s2-dev/streamstore': specifier: ^0.25.0 version: 0.25.0(supports-color@10.0.0) @@ -1709,9 +1591,6 @@ importers: git-last-commit: specifier: ^1.0.1 version: 1.0.1 - gradient-string: - specifier: ^2.0.2 - version: 2.0.2 has-flag: specifier: ^5.0.1 version: 5.0.1 @@ -1721,9 +1600,6 @@ importers: import-in-the-middle: specifier: 3.0.1 version: 3.0.1 - import-meta-resolve: - specifier: ^4.1.0 - version: 4.1.0 ini: specifier: ^5.0.0 version: 5.0.0 @@ -1745,9 +1621,6 @@ importers: nypm: specifier: ^0.5.4 version: 0.5.4 - object-hash: - specifier: ^3.0.0 - version: 3.0.0 open: specifier: ^10.0.3 version: 10.0.3 @@ -1757,9 +1630,6 @@ importers: p-retry: specifier: ^6.1.0 version: 6.1.0 - partysocket: - specifier: ^1.0.2 - version: 1.0.2 pkg-types: specifier: ^1.1.3 version: 1.1.3 @@ -1790,18 +1660,12 @@ importers: tar: specifier: 7.5.21 version: 7.5.21 - tiny-invariant: - specifier: ^1.2.0 - version: 1.3.1 tinyexec: specifier: ^0.3.1 version: 0.3.1 tinyglobby: specifier: ^0.2.10 version: 0.2.10 - ws: - specifier: 8.21.0 - version: 8.21.0(bufferutil@4.0.9) xdg-app-paths: specifier: ^8.3.0 version: 8.3.0 @@ -1815,39 +1679,18 @@ importers: '@epic-web/test-server': specifier: ^0.1.0 version: 0.1.0(bufferutil@4.0.9) - '@types/eventsource': - specifier: ^1.1.15 - version: 1.1.15 - '@types/gradient-string': - specifier: ^1.1.2 - version: 1.1.2 '@types/ini': specifier: ^4.1.1 version: 4.1.1 - '@types/object-hash': - specifier: 3.0.6 - version: 3.0.6 - '@types/react': - specifier: ^18.2.48 - version: 18.2.48 '@types/resolve': specifier: ^1.20.6 version: 1.20.6 - '@types/rimraf': - specifier: ^4.0.5 - version: 4.0.5 '@types/semver': specifier: ^7.5.0 version: 7.5.1 '@types/source-map-support': specifier: 0.5.10 version: 0.5.10 - '@types/ws': - specifier: ^8.5.3 - version: 8.5.4 - cpy-cli: - specifier: ^5.0.0 - version: 5.0.0 execa: specifier: ^8.0.1 version: 8.0.1 @@ -1857,9 +1700,6 @@ importers: rimraf: specifier: ^6.0.1 version: 6.0.1 - ts-essentials: - specifier: 10.0.1 - version: 10.0.1(typescript@7.0.2) tshy: specifier: ^4.1.3 version: 4.1.3 @@ -1920,9 +1760,6 @@ importers: '@opentelemetry/sdk-trace-node': specifier: 2.7.1 version: 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': - specifier: 1.41.1 - version: 1.41.1 '@s2-dev/streamstore': specifier: 0.25.0 version: 0.25.0(supports-color@10.0.0) @@ -1935,9 +1772,6 @@ importers: eventsource-parser: specifier: ^3.0.0 version: 3.0.0 - execa: - specifier: ^8.0.1 - version: 8.0.1 humanize-duration: specifier: ^3.27.3 version: 3.27.3 @@ -1965,16 +1799,10 @@ importers: zod: specifier: 3.25.76 version: 3.25.76 - zod-error: - specifier: 1.5.0 - version: 1.5.0 zod-validation-error: specifier: ^1.5.0 version: 1.5.0(zod@3.25.76) devDependencies: - '@ai-sdk/provider-utils': - specifier: ^1.0.22 - version: 1.0.22(zod@3.25.76) '@arethetypeswrong/cli': specifier: ^0.18.5 version: 0.18.5 @@ -1990,12 +1818,6 @@ importers: '@types/humanize-duration': specifier: ^3.27.1 version: 3.27.1 - '@types/lodash.get': - specifier: ^4.4.9 - version: 4.4.9 - '@types/readable-stream': - specifier: ^4.0.14 - version: 4.0.14 ai: specifier: 6.0.116 version: 6.0.116(zod@3.25.76) @@ -2067,9 +1889,6 @@ importers: '@types/node': specifier: 24.13.3 version: 24.13.3 - esbuild: - specifier: ^0.23.0 - version: 0.23.0 rimraf: specifier: 6.0.1 version: 6.0.1 @@ -2125,9 +1944,6 @@ importers: cron-parser: specifier: ^4.9.0 version: 4.9.0 - lodash.omit: - specifier: ^4.5.0 - version: 4.5.0 nanoid: specifier: ^5.1.16 version: 5.1.16 @@ -2150,9 +1966,6 @@ importers: '@internal/tracing': specifier: workspace:* version: link:../../internal-packages/tracing - '@types/lodash.omit': - specifier: ^4.5.7 - version: 4.5.7 '@types/seedrandom': specifier: ^3.0.8 version: 3.0.8 @@ -2187,18 +2000,9 @@ importers: '@arethetypeswrong/cli': specifier: ^0.18.5 version: 0.18.5 - '@trigger.dev/build': - specifier: workspace:^4.5.11 - version: link:../build '@types/node': specifier: 24.13.3 version: 24.13.3 - '@types/react': - specifier: '*' - version: 18.3.1 - '@types/react-dom': - specifier: '*' - version: 18.2.7 rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -2269,33 +2073,12 @@ importers: '@trigger.dev/core': specifier: workspace:4.5.11 version: link:../core - chalk: - specifier: ^5.2.0 - version: 5.2.0 - cronstrue: - specifier: ^2.21.0 - version: 2.21.0 - debug: - specifier: ^4.3.4 - version: 4.3.4 - evt: - specifier: ^2.4.13 - version: 2.4.13 react: specifier: 18.3.1 version: 18.3.1 - slug: - specifier: ^6.0.0 - version: 6.1.0 - ulid: - specifier: ^2.3.0 - version: 2.3.0 uncrypto: specifier: ^0.1.3 version: 0.1.3 - ws: - specifier: 8.21.0 - version: 8.21.0(bufferutil@4.0.9) devDependencies: '@ai-sdk/provider': specifier: 3.0.8 @@ -2303,27 +2086,15 @@ importers: '@arethetypeswrong/cli': specifier: ^0.18.5 version: 0.18.5 - '@types/debug': - specifier: ^4.1.7 - version: 4.1.7 '@types/react': specifier: ^19.2.14 version: 19.2.14 - '@types/slug': - specifier: ^5.0.3 - version: 5.0.3 - '@types/ws': - specifier: ^8.5.3 - version: 8.5.4 ai: specifier: 6.0.116 version: 6.0.116(zod@3.25.76) ai-v7: specifier: npm:ai@7.0.0-canary.159 version: ai@7.0.0-canary.159(zod@3.25.76) - encoding: - specifier: ^0.1.13 - version: 0.1.13 rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -2333,9 +2104,6 @@ importers: tsx: specifier: 4.17.0 version: 4.17.0 - typed-emitter: - specifier: ^2.1.0 - version: 2.1.0 typescript: specifier: 'catalog:' version: 7.0.2 @@ -2379,15 +2147,6 @@ packages: resolution: {integrity: sha512-K5VikyO3EKQkNk77ew9oMjM8FInKF+WWar599LmP8rQ0x0iB+P/DVS+h6zQvmecxMNPtQOOyt0uDQFx/AA0DGw==} engines: {node: '>=18'} - '@ai-sdk/provider-utils@1.0.22': - resolution: {integrity: sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - '@ai-sdk/provider-utils@4.0.29': resolution: {integrity: sha512-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA==} engines: {node: '>=18'} @@ -2406,10 +2165,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider@0.0.26': - resolution: {integrity: sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==} - engines: {node: '>=18'} - '@ai-sdk/provider@3.0.10': resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} engines: {node: '>=18'} @@ -4700,10 +4455,6 @@ packages: resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} - '@manypkg/cli@0.19.2': - resolution: {integrity: sha512-DXx/P1lyunNoFWwOj1MWBucUhaIJljoiAGOpO2fE0GKMBCI6EZBZD0Up1+fQZoXBecKXRgV9mGgLvIB2fOQ0KQ==} - hasBin: true - '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -5015,12 +4766,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-fetch@0.218.0': - resolution: {integrity: sha512-eP/Y5hDupb+6MwZSaMw4ZdsDz8YgfJbJ4Ta86BMVeOmI2EArwXcd0v1nfNIvUzXTPi7nakidwqeuUa3FwRwECg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-fs@0.19.1': resolution: {integrity: sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==} engines: {node: '>=14'} @@ -5217,12 +4962,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-web@2.7.1': - resolution: {integrity: sha512-K806OouCSOjMd8Nr7+ZCq3QT22tdAzzS/7h8vprfiKjkgFQ99/dvwU8d12WJANA6D5Qtme65hyBAqAu9CkQuxQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/semantic-conventions@1.28.0': resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} engines: {node: '>=14'} @@ -6754,16 +6493,6 @@ packages: typescript: optional: true - '@remix-run/testing@2.17.5': - resolution: {integrity: sha512-WrGoMoitoRlwpdRmDQkiIdzCeKxEhPSs2+wQi+FR3syh07gGC+8M2ogLMY7Kskqgjgs2mqDaFmQGuP3WT+eIig==} - engines: {node: '>=18.0.0'} - peerDependencies: - react: 18.3.1 - typescript: ^5.1.0 - peerDependenciesMeta: - typescript: - optional: true - '@remix-run/web-blob@3.1.0': resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} @@ -7172,10 +6901,6 @@ packages: '@sinclair/typebox@0.34.38': resolution: {integrity: sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==} - '@sindresorhus/is@0.14.0': - resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} - engines: {node: '>=6'} - '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -7623,87 +7348,12 @@ packages: '@stricli/core@1.2.0': resolution: {integrity: sha512-5b+npntDY0TAB7wAw0daGlh3/R2sf0TDLyrB1By2jCNH+C+lmcSqMtJXOMLVtEGSkIOvqAgIWpLMSs1PXqzt3w==} - '@swc/core-darwin-arm64@1.3.26': - resolution: {integrity: sha512-FWWflBfKRYrUJtko2xiedC5XCa31O75IZZqnTWuLpe9g3C5tnUuF3M8LSXZS/dn6wprome1MhtG9GMPkSYkhkg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [darwin] - - '@swc/core-darwin-x64@1.3.26': - resolution: {integrity: sha512-0uQeebAtsewqJ2b35aPZstGrylwd6oJjUyAJOfVJNbremFSJ5JzytB3NoDCIw7CT5UQrSRpvD3mU95gfdQjDGA==} - engines: {node: '>=10'} - cpu: [x64] - os: [darwin] - - '@swc/core-linux-arm-gnueabihf@1.3.26': - resolution: {integrity: sha512-06T+LbVFlyciQtwrUB5/a16A1ju1jFoYvd/hq9TWhf7GrtL43U7oJIgqMOPHx2j0+Ps2R3S6R/UUN5YXu618zA==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux] - - '@swc/core-linux-arm64-gnu@1.3.26': - resolution: {integrity: sha512-2NT/0xALPfK+U01qIlHxjkGdIj6F0txhu1U2v6B0YP2+k0whL2gCgYeg9QUvkYEXSD5r1Yx+vcb2R/vaSCSClg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@swc/core-linux-arm64-musl@1.3.26': - resolution: {integrity: sha512-64KrTay9hC0mTvZ1AmEFmNEwV5QDjw9U7PJU5riotSc28I+Q/ZoM0qcSFW9JRRa6F2Tr+IfMtyv8+eB2//BQ5g==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@swc/core-linux-x64-gnu@1.3.26': - resolution: {integrity: sha512-Te8G13l3dcRM1Mf3J4JzGUngzNXLKnMYlUmBOYN/ORsx7e+VNelR3zsTLHC0+0jGqELDgqvMyzDfk+dux/C/bQ==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@swc/core-linux-x64-musl@1.3.26': - resolution: {integrity: sha512-nqQWuSM6OTKepUiQ9+rXgERq/JiO72RBOpXKO2afYppsL96sngjIRewV74v5f6IAfyzw+k+AhC5pgRA4Xu/Jkg==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@swc/core-win32-arm64-msvc@1.3.26': - resolution: {integrity: sha512-xx34mx+9IBV1sun7sxoNFiqNom9wiOuvsQFJUyQptCnZHgYwOr9OI204LBF95dCcBCZsTm2hT1wBnySJOeimYw==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] - - '@swc/core-win32-ia32-msvc@1.3.26': - resolution: {integrity: sha512-48LZ/HKNuU9zl8c7qG6IQKb5rBCwmJgysGOmEGzTRBYxAf/x6Scmt0aqxCoV4J02HOs2WduCBDnhUKsSQ2kcXQ==} - engines: {node: '>=10'} - cpu: [ia32] - os: [win32] - - '@swc/core-win32-x64-msvc@1.3.26': - resolution: {integrity: sha512-UPe7S+MezD/S6cKBIc50TduGzmw6PBz1Ms5p+5wDLOKYNS/LSEM4iRmLwvePzP5X8mOyesXrsbwxLy8KHP65Yw==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] - - '@swc/core@1.3.26': - resolution: {integrity: sha512-U7vEsaLn3IGg0XCRLJX/GTkK9WIfFHUX5USdrp1L2QD29sWPe25HqNndXmUR9KytzKmpDMNoUuHyiuhpVrnNeQ==} - engines: {node: '>=10'} - - '@swc/helpers@0.4.14': - resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} '@swc/helpers@0.5.2': resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} - '@szmarczak/http-timer@1.1.2': - resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} - engines: {node: '>=6'} - '@tabler/icons-react@3.36.1': resolution: {integrity: sha512-/8nOXeNeMoze9xY/QyEKG65wuvRhkT3q9aytaur6Gj8bYU2A98YVJyLc9MRmc5nVvpy+bRlrrwK/Ykr8WGyUWg==} peerDependencies: @@ -7886,9 +7536,6 @@ packages: '@types/aws-lambda@8.10.152': resolution: {integrity: sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==} - '@types/bcryptjs@2.4.2': - resolution: {integrity: sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==} - '@types/body-parser@1.19.2': resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} @@ -8012,13 +7659,6 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - '@types/debug@4.1.7': - resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} - - '@types/decimal.js@7.4.3': - resolution: {integrity: sha512-7MpxcJPHqQ637FCZwJLtJMaDZkcD/iyUxj0m8A+m06slFeqRiK9QtgEyuocWNRbEtCrOZOEbZPTSSR88hMZVsg==} - deprecated: This is a stub types definition. decimal.js provides its own type definitions, so you do not need this installed. - '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -8031,12 +7671,6 @@ packages: '@types/dockerode@4.0.1': resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} - '@types/eslint-scope@3.7.7': - resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - - '@types/eslint@8.56.12': - resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} - '@types/estree-jsx@1.0.0': resolution: {integrity: sha512-3qvGd0z8F2ENTGr/GG1yViqfiKmRfrXVx5sJyHGFu3z7m5g5utCQtGp/g29JnjflhtQJBv1WDQukHiT58xPcYQ==} @@ -8046,9 +7680,6 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/eventsource@1.1.15': - resolution: {integrity: sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==} - '@types/express-serve-static-core@4.17.32': resolution: {integrity: sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==} @@ -8058,9 +7689,6 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/gradient-string@1.1.2': - resolution: {integrity: sha512-zIet2KvHr2dkOCPI5ggQQ+WJVyfBSFaqK9sNelhgDjlE2K3Fu2muuPJwu5aKM3xoWuc3WXudVEMUwI1QWhykEQ==} - '@types/hast@2.3.4': resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==} @@ -8082,30 +7710,9 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - '@types/json-query@2.2.3': - resolution: {integrity: sha512-ygE4p8lyKzTBo9LF2K/u6MHnxPxbHY6wGvwM7TdAKhbP3SvEf+Y9aeVWedDiP8SMIPowTl9R/6awQYjiUTHz2g==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - '@types/keyv@3.1.4': - resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - - '@types/lodash.get@4.4.9': - resolution: {integrity: sha512-J5dvW98sxmGnamqf+/aLP87PYXyrha9xIgc2ZlHl6OHMFR2Ejdxep50QfU0abO1+CH6+ugx+8wEUN1toImAinA==} - - '@types/lodash.omit@4.5.7': - resolution: {integrity: sha512-6q6cNg0tQ6oTWjSM+BcYMBhan54P/gLqBldG4AuXd3nKr0oeVekWNS4VrNEu3BhCSDXtGapi7zjhnna0s03KpA==} - - '@types/lodash@4.14.191': - resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} - '@types/marked@4.0.8': resolution: {integrity: sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==} @@ -8139,9 +7746,6 @@ packages: '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} - '@types/node-fetch@2.6.2': - resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} - '@types/node-fetch@2.6.4': resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} @@ -8154,9 +7758,6 @@ packages: '@types/normalize-package-data@2.4.1': resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} - '@types/object-hash@3.0.6': - resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==} - '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} @@ -8181,9 +7782,6 @@ packages: '@types/react-dom@18.2.7': resolution: {integrity: sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==} - '@types/react@18.2.48': - resolution: {integrity: sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==} - '@types/react@18.2.69': resolution: {integrity: sha512-W1HOMUWY/1Yyw0ba5TkCV+oqynRjG7BnteBB+B7JmAK7iw3l2SW+VGOxL+akPweix6jk2NNJtyJKpn4TkpfK3Q==} @@ -8193,37 +7791,24 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - '@types/readable-stream@4.0.14': - resolution: {integrity: sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==} - '@types/regression@2.0.6': resolution: {integrity: sha512-sa+sHOUxh9fywFuAFLCcyupFN0CKX654QUZGW5fAZCmV51I4e5nQy1xL2g/JMUW/PeDoF3Yq2lDXb7MoC3KDNg==} '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} - '@types/responselike@1.0.0': - resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} - '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} - '@types/rimraf@4.0.5': - resolution: {integrity: sha512-DTCZoIQotB2SUJnYgrEx43cQIUYOlNZz0AZPbKU4PSLYTUdML5Gox0++z4F9kQocxStrCmRNhi4x5x/UlwtKUA==} - deprecated: This is a stub types definition. rimraf provides its own type definitions, so you do not need this installed. - '@types/scheduler@0.16.2': resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} '@types/seedrandom@3.0.8': resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==} - '@types/semver@6.2.3': - resolution: {integrity: sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A==} - '@types/semver@7.5.1': resolution: {integrity: sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==} @@ -8263,9 +7848,6 @@ packages: '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} - '@types/tinycolor2@1.4.3': - resolution: {integrity: sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==} - '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -8562,51 +8144,6 @@ packages: '@web3-storage/multipart-parser@1.0.0': resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} - '@webassemblyjs/ast@1.14.1': - resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - - '@webassemblyjs/floating-point-hex-parser@1.13.2': - resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - - '@webassemblyjs/helper-api-error@1.13.2': - resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - - '@webassemblyjs/helper-buffer@1.14.1': - resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - - '@webassemblyjs/helper-numbers@1.13.2': - resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': - resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - - '@webassemblyjs/helper-wasm-section@1.14.1': - resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - - '@webassemblyjs/ieee754@1.13.2': - resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - - '@webassemblyjs/leb128@1.13.2': - resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - - '@webassemblyjs/utf8@1.13.2': - resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - - '@webassemblyjs/wasm-edit@1.14.1': - resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - - '@webassemblyjs/wasm-gen@1.14.1': - resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - - '@webassemblyjs/wasm-opt@1.14.1': - resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - - '@webassemblyjs/wasm-parser@1.14.1': - resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - - '@webassemblyjs/wast-printer@1.14.1': - resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - '@window-splitter/interface@1.1.3': resolution: {integrity: sha512-GV7nunGpSqrlbR8pyI65aFMYlyFTO1VgWhT2cFsPkfYwmh5xBNWAWkJJtDMvbfwBHtvTGf4kTztx6e9LZSiSeQ==} engines: {node: '>=18.0.0'} @@ -8632,12 +8169,6 @@ packages: '@xobotyi/scrollbar-width@1.9.5': resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} - '@xtuc/ieee754@1.2.0': - resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - - '@xtuc/long@4.2.2': - resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - '@yuku-codegen/binding-darwin-arm64@0.7.2': resolution: {integrity: sha512-SUE7nUmiPmr/H6qUUgsKtXY3wCtKsIeru3MSUKr4rVzZ/Q/zzNwdCP7WiOHQKEhjvXAcOedAx0VXJ/0ORpqUVA==} cpu: [arm64] @@ -8790,12 +8321,6 @@ packages: peerDependencies: acorn: ^8 - acorn-import-phases@1.0.4: - resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} - engines: {node: '>=10.13.0'} - peerDependencies: - acorn: ^8.14.0 - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -8833,10 +8358,6 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - aggregate-error@4.0.1: - resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} - engines: {node: '>=12'} - ahocorasick@1.0.2: resolution: {integrity: sha512-hCOfMzbFx5IDutmWLAt6MZwOUjIfSM9G9FyVxytmE4Rs/5YDPWQrD/+IR1w+FweD9H2oOZEnv36TmkjhNURBVA==} @@ -8858,27 +8379,14 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: ajv: ^8.18.0 peerDependenciesMeta: ajv: optional: true - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.18.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv-keywords@5.1.0: - resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} - peerDependencies: - ajv: ^8.18.0 - ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} @@ -8982,10 +8490,6 @@ packages: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} - arrify@3.0.0: - resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} - engines: {node: '>=12'} - asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -9032,13 +8536,6 @@ packages: autoevals@0.0.130: resolution: {integrity: sha512-JS0T/YCEH13AAOGiWWGJDkIPP8LsDmRBYr3EazTukHxvd0nidOW7fGj0qVPFx2bARrSNO9AfCR6xoTP/5m3Bmw==} - autoprefixer@10.4.13: - resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.5.23 - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -9189,11 +8686,6 @@ packages: browserify-zlib@0.1.4: resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} - browserslist@4.21.4: - resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -9226,9 +8718,6 @@ packages: resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} engines: {node: '>=10.0.0'} - builtins@1.0.3: - resolution: {integrity: sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==} - builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} @@ -9276,10 +8765,6 @@ packages: resolution: {integrity: sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - cacheable-request@6.1.0: - resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} - engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -9292,10 +8777,6 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} engines: {node: '>=8'} @@ -9304,9 +8785,6 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - caniuse-lite@1.0.30001577: - resolution: {integrity: sha512-rs2ZygrG1PNXMfmncM0B5H1hndY5ZCC9b5TkFaVNfZ+AUlyqcMyVIQtc3fsezi0NUCk5XZfDf9WS6WxMxnfdrg==} - caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} @@ -9329,10 +8807,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.2.0: - resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - chalk@5.3.0: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -9378,10 +8852,6 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} - chrome-trace-event@1.0.4: - resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} - engines: {node: '>=6.0'} - ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} @@ -9410,10 +8880,6 @@ packages: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} - clean-stack@4.2.0: - resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} - engines: {node: '>=12'} - cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -9448,9 +8914,6 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - clone-response@1.0.3: - resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} - clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -9622,32 +9085,10 @@ packages: cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - - cp-file@10.0.0: - resolution: {integrity: sha512-vy2Vi1r2epK5WqxOLnskeKeZkdZvTKfFZQCplE3XWsP+SUJyd5XAUFC9lFgTjjXJF2GMne/UML14iEmkAaDfFg==} - engines: {node: '>=14.16'} - cpu-features@0.0.10: resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} engines: {node: '>=10.0.0'} - cpy-cli@5.0.0: - resolution: {integrity: sha512-fb+DZYbL9KHc0BC4NYqGRrDIJZPXUmjjtqdw4XRRg8iV8dIfghUX/WiL+q4/B/KFTy3sK6jsbUhBaz0/Hxg7IQ==} - engines: {node: '>=16'} - hasBin: true - - cpy@10.1.0: - resolution: {integrity: sha512-VC2Gs20JcTyeQob6UViBLnyP0bYHkBh6EiKzot9vi2DmeGlFT9Wd7VG3NBrkNx/jYvFBeyDOMMHdHQhbtKLgHQ==} - engines: {node: '>=16'} - crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -9668,11 +9109,6 @@ packages: resolution: {integrity: sha512-YxabE1ZSHA1zJZMPCTSEbc0u4cRRenjqqTgCwJT7OvkspPSvfYFITuPFtsT+VkBuavJtFv2kJXT+mKSnlUJxfg==} hasBin: true - cronstrue@2.61.0: - resolution: {integrity: sha512-ootN5bvXbIQI9rW94+QsXN5eROtXWwew6NkdGxIRpS/UFWRggL0G5Al7a9GTBFEsuvVhJ2K3CntIIVt7L2ILhA==} - deprecated: Non-backwards compatible Breaking changes - hasBin: true - cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -9698,18 +9134,6 @@ packages: css-in-js-utils@3.1.0: resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} - css-loader@6.10.0: - resolution: {integrity: sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==} - engines: {node: '>= 12.13.0'} - peerDependencies: - '@rspack/core': 0.x || 1.x - webpack: ^5.0.0 - peerDependenciesMeta: - '@rspack/core': - optional: true - webpack: - optional: true - css-tree@1.1.3: resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} engines: {node: '>=8.0.0'} @@ -9727,9 +9151,6 @@ packages: engines: {node: '>=4'} hasBin: true - csstype@3.1.1: - resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} - csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -10006,10 +9427,6 @@ packages: decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} - decompress-response@3.3.0: - resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} - engines: {node: '>=4'} - decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -10040,9 +9457,6 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - defer-to-connect@1.1.3: - resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} - define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -10291,9 +9705,6 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - duplexer3@0.1.5: - resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} - duplexify@3.7.1: resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} @@ -10326,9 +9737,6 @@ packages: effect@3.21.2: resolution: {integrity: sha512-rXd2FGDM8KdjSIrc+mqEELo7ScW7xTVxEf1iInmPSpIde9/nyGuFM710cjTo7/EreGXiUX2MOonPpprbz2XHCg==} - electron-to-chromium@1.4.433: - resolution: {integrity: sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==} - electron-to-chromium@1.5.325: resolution: {integrity: sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==} @@ -10393,10 +9801,6 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -10631,27 +10035,11 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - estree-util-attach-comments@2.1.0: resolution: {integrity: sha512-rJz6I4L0GaXYtHpoMScgDIwM0/Vwbu5shbMeER596rB2D1EWF6+Gj0e0UKzJPZrpoOc87+Q2kgVFHfjAymIqmw==} @@ -10704,10 +10092,6 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - event-target-shim@6.0.2: - resolution: {integrity: sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==} - engines: {node: '>=10.13.0'} - eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -10718,10 +10102,6 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - eventsource-parser@1.1.2: - resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} - engines: {node: '>=14.18'} - eventsource-parser@3.0.0: resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==} engines: {node: '>=18.0.0'} @@ -11006,9 +10386,6 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - fraction.js@4.2.0: - resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} - framer-motion@10.12.11: resolution: {integrity: sha512-uNsJAc/BQZ9V7tYgzRBXSrEdB+YrdJTtRvgn+8lNAQucGKaINJBL8I4aqXxXdw+HzwJZb/uR955jnOrxBy5sTA==} peerDependencies: @@ -11098,14 +10475,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@4.1.0: - resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} - engines: {node: '>=6'} - - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -11150,9 +10519,6 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -11184,10 +10550,6 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - globby@13.2.2: - resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} @@ -11195,17 +10557,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - got@9.6.0: - resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} - engines: {node: '>=8.6'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - gradient-string@2.0.2: - resolution: {integrity: sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==} - engines: {node: '>=10'} - grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} @@ -11335,9 +10689,6 @@ packages: htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -11403,10 +10754,6 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} @@ -11414,9 +10761,6 @@ packages: resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} engines: {node: '>=18'} - import-meta-resolve@4.1.0: - resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} - import-without-cache@0.4.0: resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} engines: {node: ^22.18.0 || >=24.0.0} @@ -11429,10 +10773,6 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - indent-string@5.0.0: - resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} - engines: {node: '>=12'} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -11734,14 +11074,6 @@ packages: javascript-stringify@2.1.0: resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} - jest-worker@27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} - - jiti@1.21.0: - resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} - hasBin: true - jiti@1.21.6: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true @@ -11802,9 +11134,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-buffer@3.0.0: - resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} - json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -11828,10 +11157,6 @@ packages: resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} engines: {node: '>= 0.4'} - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -11857,18 +11182,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - jsonpointer@5.0.1: - resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} - engines: {node: '>=0.10.0'} - jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} - junk@4.0.1: - resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} - engines: {node: '>=12.20'} - jwa@1.4.2: resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} @@ -11882,9 +11199,6 @@ packages: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} hasBin: true - keyv@3.1.0: - resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} - khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} @@ -12066,10 +11380,6 @@ packages: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} - loader-runner@4.3.1: - resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} - engines: {node: '>=6.11.5'} - loader-utils@3.2.1: resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==} engines: {node: '>= 12.13.0'} @@ -12164,14 +11474,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lowercase-keys@1.0.1: - resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} - engines: {node: '>=0.10.0'} - - lowercase-keys@2.0.0: - resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} - engines: {node: '>=8'} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -12362,10 +11664,6 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} - meow@12.1.1: - resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} - engines: {node: '>=16.10'} - meow@6.1.1: resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} engines: {node: '>=8'} @@ -12609,10 +11907,6 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - mimic-response@1.0.1: - resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} - engines: {node: '>=4'} - mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -12655,9 +11949,6 @@ packages: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} - minimist@1.2.7: - resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -12801,12 +12092,6 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - - nested-error-stacks@2.1.1: - resolution: {integrity: sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==} - neverthrow@8.2.0: resolution: {integrity: sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==} engines: {node: '>=18'} @@ -12839,22 +12124,10 @@ packages: encoding: optional: true - node-fetch@2.6.7: - resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-releases@2.0.12: - resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} - node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} @@ -12881,14 +12154,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - - normalize-url@4.5.1: - resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} - engines: {node: '>=8'} - notepack.io@3.0.1: resolution: {integrity: sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg==} @@ -12947,10 +12212,6 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -13094,22 +12355,10 @@ packages: vite-plus: optional: true - p-cancelable@1.1.0: - resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} - engines: {node: '>=6'} - - p-event@5.0.1: - resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} - p-filter@3.0.0: - resolution: {integrity: sha512-QtoWLjXAW++uTX67HZQz1dbTpqBfiidsB6VtQUC9iR85S120+s0T5sO6s+B5MLzFcZkrEd/DGMmCjR+f2Qpxwg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -13150,10 +12399,6 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} - p-map@5.5.0: - resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==} - engines: {node: '>=12'} - p-map@6.0.0: resolution: {integrity: sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw==} engines: {node: '>=16'} @@ -13174,10 +12419,6 @@ packages: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} engines: {node: '>=8'} - p-timeout@5.1.0: - resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==} - engines: {node: '>=12'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -13185,31 +12426,18 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - package-json@6.5.0: - resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} - engines: {node: '>=8'} - package-manager-detector@1.4.1: resolution: {integrity: sha512-dSMiVLBEA4XaNJ0PRb4N5cV/SEP4BWrWZKBmfF+OUm2pQTiZ6DDkKeWaltwu3JRhLoy59ayIkJ00cx9K9CaYTg==} pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - parse-duration@2.1.4: resolution: {integrity: sha512-b98m6MsCh+akxfyoz9w9dt0AlH2dfYLOBss5SdDsr9pkhKNvkWBXU/r8A4ahmIGByBOLV2+4YwfCuFxbDDaGyg==} parse-entities@4.0.0: resolution: {integrity: sha512-5nk9Fn03x3rEhGaX1FU6IDwG/k+GxLXlFAkgrbM1asuAFl3BhdQWvASaIsmwWypRNcZKHPYnIuOSfIWEyEQnPQ==} - parse-github-url@1.0.2: - resolution: {integrity: sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==} - engines: {node: '>=0.10.0'} - hasBin: true - parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -13241,9 +12469,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - partysocket@1.0.2: - resolution: {integrity: sha512-rAFOUKImaq+VBk2B+2RTBsWEvlnarEP53nchoUHzpVs8V6fG2/estihOTslTQUWHVuHEKDL5k8htG8K3TngyFA==} - path-data-parser@0.1.0: resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} @@ -13353,9 +12578,6 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -13380,10 +12602,6 @@ packages: engines: {node: '>=0.10'} hasBin: true - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -13444,12 +12662,6 @@ packages: peerDependencies: postcss: ^8.5.23 - postcss-import@16.0.1: - resolution: {integrity: sha512-i2Pci0310NaLHr/5JUFSw1j/8hf1CzwMY13g6ZDxgOavmRHQi2ba3PmUHoihO+sjaum+KmCNzskNsw7JDrg03g==} - engines: {node: '>=18.0.0'} - peerDependencies: - postcss: ^8.5.23 - postcss-load-config@4.0.2: resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} engines: {node: '>= 14'} @@ -13462,19 +12674,6 @@ packages: ts-node: optional: true - postcss-loader@8.1.1: - resolution: {integrity: sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==} - engines: {node: '>= 18.12.0'} - peerDependencies: - '@rspack/core': 0.x || 1.x - postcss: ^8.5.23 - webpack: ^5.0.0 - peerDependenciesMeta: - '@rspack/core': - optional: true - webpack: - optional: true - postcss-modules-extract-imports@3.0.0: resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} engines: {node: ^10 || ^12 || >= 14} @@ -13581,10 +12780,6 @@ packages: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} engines: {node: '>=10'} - prepend-http@2.0.0: - resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} - engines: {node: '>=4'} - prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -13941,9 +13136,6 @@ packages: resolution: {integrity: sha512-NZKln+uyPuyHchzP07I6GGYFxdAoaKhehgpCa3ltJGzwE31OYumLeshGaitA1R/fS5d9D2qpZVwTFAr6zCLM9w==} engines: {node: '>=0.10.0'} - read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -14038,14 +13230,6 @@ packages: reghex@3.0.2: resolution: {integrity: sha512-Zb9DJ5u6GhgqRSBnxV2QSnLqEwcKxHWFA1N2yUa4ZUAO1P8jlWKYtWZ6/ooV6yylspGXJX0O/uNzEv0xrCtwaA==} - registry-auth-token@4.2.2: - resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} - engines: {node: '>=6.0.0'} - - registry-url@5.1.0: - resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} - engines: {node: '>=8'} - regression@2.0.1: resolution: {integrity: sha512-A4XYsc37dsBaNOgEjkJKzfJlE394IMmUPlI/p3TTI9u3T+2a+eox5Pr/CPUqF0eszeWZJPAc6QkroAhuUpWDJQ==} @@ -14194,10 +13378,6 @@ packages: resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -14217,9 +13397,6 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true - responselike@1.0.2: - resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} - restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -14364,17 +13541,10 @@ packages: scheduler@0.25.0-rc.1: resolution: {integrity: sha512-fVinv2lXqYpKConAMdergOl5owd0rY1O4P/QTe0aWKCqGtu7VsCt1iqQFxSJtqK4Lci/upVSBpGwVC7eWcuS9Q==} - schema-utils@4.3.3: - resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} - engines: {node: '>= 10.13.0'} - screenfull@5.2.0: resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} engines: {node: '>=0.10.0'} - secure-json-parse@2.7.0: - resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} - secure-json-parse@4.0.0: resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==} @@ -14384,9 +13554,6 @@ packages: selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} - sembear@0.5.2: - resolution: {integrity: sha512-Ij1vCAdFgWABd7zTg50Xw1/p0JgESNxuLlneEAsmBrKishA06ulTTL/SHGmNy2Zud7+rKrHTKNI6moJsn1ppAQ==} - semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -14527,10 +13694,6 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - slash@4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} - slice-ansi@4.0.0: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} @@ -14780,12 +13943,6 @@ packages: stubborn-utils@1.0.2: resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} - style-loader@3.3.4: - resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^5.0.0 - style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} @@ -14834,10 +13991,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - supports-hyperlinks@3.1.0: resolution: {integrity: sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==} engines: {node: '>=14.18'} @@ -14918,22 +14071,6 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - terser-webpack-plugin@5.4.0: - resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - terser@5.46.1: resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} engines: {node: '>=10'} @@ -14976,9 +14113,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinycolor2@1.6.0: - resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@0.3.0: resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} @@ -15004,9 +14138,6 @@ packages: resolution: {integrity: sha512-mZ2sDMaySvi1PkTp4lTo1In2zjU+cY8OvZsfwrDrx3YGRbXPX1/cbPwCR9zkm3O/Fz9Jo0F1HNgIQ1b8BepqyQ==} engines: {node: '>=12.0.0'} - tinygradient@1.1.5: - resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} - tinypool@2.1.0: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} @@ -15033,10 +14164,6 @@ packages: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} - to-readable-stream@1.0.0: - resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} - engines: {node: '>=6'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -15110,17 +14237,6 @@ packages: tsafe@1.4.1: resolution: {integrity: sha512-3IDBalvf6SyvHFS14UiwCWzqdSdo+Q0k2J7DZyJYaHW/iraW9DJpaBKDJpry3yQs3o/t/A+oGaRW3iVt2lKxzA==} - tsconfck@2.1.2: - resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} - engines: {node: ^14.13.1 || ^16 || >=18} - deprecated: unmaintained - hasBin: true - peerDependencies: - typescript: ^4.3.5 || ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - tsconfck@3.1.3: resolution: {integrity: sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ==} engines: {node: ^18 || >=20} @@ -15132,9 +14248,6 @@ packages: typescript: optional: true - tsconfig-paths@3.14.1: - resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} - tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -15178,9 +14291,6 @@ packages: engines: {node: 20 || >=22} hasBin: true - tslib@2.4.1: - resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} - tslib@2.5.0: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} @@ -15312,9 +14422,6 @@ packages: resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} engines: {node: '>= 0.4'} - typed-emitter@2.1.0: - resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==} - typescript@5.6.1-rc: resolution: {integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==} engines: {node: '>=14.17'} @@ -15353,10 +14460,6 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} - ulid@2.3.0: - resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} - hasBin: true - unbash@4.0.2: resolution: {integrity: sha512-8gwNZ29+0/3zmXw7ToIHZtg6wK37xnniRUdBt7B27xZxaxfgR5tGMaGHT0t0dLtBV9fXE7zurh0s6Z1DHVjfWg==} engines: {node: '>=14'} @@ -15468,22 +14571,12 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - update-browserslist-db@1.0.11: - resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - url-parse-lax@3.0.0: - resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} - engines: {node: '>=4'} - use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -15576,9 +14669,6 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - validate-npm-package-name@3.0.0: - resolution: {integrity: sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==} - validate-npm-package-name@5.0.0: resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -15624,9 +14714,6 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-tsconfig-paths@4.0.5: - resolution: {integrity: sha512-/L/eHwySFYjwxoYt1WRJniuK/jPv+WGwgRGBYx3leciR5wBeqntQpUE6Js6+TJemChc+ter7fDBKieyEWDx4yQ==} - vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: @@ -15754,10 +14841,6 @@ packages: warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} - watchpack@2.5.1: - resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} - engines: {node: '>=10.13.0'} - wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -15778,20 +14861,6 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webpack-sources@3.3.4: - resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} - engines: {node: '>=10.13.0'} - - webpack@5.102.1: - resolution: {integrity: sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -16048,15 +15117,6 @@ snapshots: transitivePeerDependencies: - zod - '@ai-sdk/provider-utils@1.0.22(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 0.0.26 - eventsource-parser: 1.1.2 - nanoid: 3.3.18 - secure-json-parse: 2.7.0 - optionalDependencies: - zod: 3.25.76 - '@ai-sdk/provider-utils@4.0.29(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.10 @@ -16079,10 +15139,6 @@ snapshots: eventsource-parser: 3.1.0 zod: 3.25.76 - '@ai-sdk/provider@0.0.26': - dependencies: - json-schema: 0.4.0 - '@ai-sdk/provider@3.0.10': dependencies: json-schema: 0.4.0 @@ -19092,6 +18148,7 @@ snapshots: dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + optional: true '@jridgewell/sourcemap-codec@1.5.5': {} @@ -19202,23 +18259,6 @@ snapshots: '@lukeed/ms@2.0.2': {} - '@manypkg/cli@0.19.2': - dependencies: - '@babel/runtime': 7.20.7 - '@manypkg/get-packages': 1.1.3 - chalk: 2.4.2 - detect-indent: 6.1.0 - find-up: 4.1.0 - fs-extra: 8.1.0 - normalize-path: 3.0.0 - p-limit: 2.3.0 - package-json: 6.5.0 - parse-github-url: 1.0.2 - sembear: 0.5.2 - semver: 6.3.1 - spawndamnit: 2.0.0 - validate-npm-package-name: 3.0.0 - '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.4 @@ -19675,16 +18715,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-fetch@0.218.0(@opentelemetry/api@1.9.1)(supports-color@10.0.0)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1)(supports-color@10.0.0) - '@opentelemetry/sdk-trace-web': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation-fs@0.19.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -19953,12 +18983,6 @@ snapshots: '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-web@2.7.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions@1.28.0': {} '@opentelemetry/semantic-conventions@1.41.1': {} @@ -21447,19 +20471,7 @@ snapshots: optionalDependencies: typescript: 7.0.2 - '@remix-run/testing@2.17.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@7.0.2)': - dependencies: - '@remix-run/node': 2.17.5(typescript@7.0.2) - '@remix-run/react': 2.17.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@7.0.2) - '@remix-run/router': 1.23.3(patch_hash=33e2966e9ef36aa09955cec9922c5063227b83429157a71ba226369a08627ac9) - react: 18.3.1 - react-router-dom: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - optionalDependencies: - typescript: 7.0.2 - transitivePeerDependencies: - - react-dom - - '@remix-run/web-blob@3.1.0': + '@remix-run/web-blob@3.1.0': dependencies: '@remix-run/web-stream': 1.1.0 web-encoding: 1.1.5 @@ -21826,8 +20838,6 @@ snapshots: '@sinclair/typebox@0.34.38': {} - '@sindresorhus/is@0.14.0': {} - '@sindresorhus/is@4.6.0': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -22544,53 +21554,6 @@ snapshots: '@stricli/core@1.2.0': {} - '@swc/core-darwin-arm64@1.3.26': - optional: true - - '@swc/core-darwin-x64@1.3.26': - optional: true - - '@swc/core-linux-arm-gnueabihf@1.3.26': - optional: true - - '@swc/core-linux-arm64-gnu@1.3.26': - optional: true - - '@swc/core-linux-arm64-musl@1.3.26': - optional: true - - '@swc/core-linux-x64-gnu@1.3.26': - optional: true - - '@swc/core-linux-x64-musl@1.3.26': - optional: true - - '@swc/core-win32-arm64-msvc@1.3.26': - optional: true - - '@swc/core-win32-ia32-msvc@1.3.26': - optional: true - - '@swc/core-win32-x64-msvc@1.3.26': - optional: true - - '@swc/core@1.3.26': - optionalDependencies: - '@swc/core-darwin-arm64': 1.3.26 - '@swc/core-darwin-x64': 1.3.26 - '@swc/core-linux-arm-gnueabihf': 1.3.26 - '@swc/core-linux-arm64-gnu': 1.3.26 - '@swc/core-linux-arm64-musl': 1.3.26 - '@swc/core-linux-x64-gnu': 1.3.26 - '@swc/core-linux-x64-musl': 1.3.26 - '@swc/core-win32-arm64-msvc': 1.3.26 - '@swc/core-win32-ia32-msvc': 1.3.26 - '@swc/core-win32-x64-msvc': 1.3.26 - - '@swc/helpers@0.4.14': - dependencies: - tslib: 2.4.1 - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -22599,10 +21562,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@szmarczak/http-timer@1.1.2': - dependencies: - defer-to-connect: 1.1.3 - '@tabler/icons-react@3.36.1(react@18.3.1)': dependencies: '@tabler/icons': 3.36.1 @@ -22765,8 +21724,6 @@ snapshots: '@types/aws-lambda@8.10.152': {} - '@types/bcryptjs@2.4.2': {} - '@types/body-parser@1.19.2': dependencies: '@types/connect': 3.4.35 @@ -22920,14 +21877,6 @@ snapshots: dependencies: '@types/ms': 0.7.31 - '@types/debug@4.1.7': - dependencies: - '@types/ms': 0.7.31 - - '@types/decimal.js@7.4.3': - dependencies: - decimal.js: 10.6.0 - '@types/deep-eql@4.0.2': {} '@types/docker-modem@3.0.6': @@ -22947,16 +21896,6 @@ snapshots: '@types/node': 24.13.3 '@types/ssh2': 1.15.1 - '@types/eslint-scope@3.7.7': - dependencies: - '@types/eslint': 8.56.12 - '@types/estree': 1.0.9 - - '@types/eslint@8.56.12': - dependencies: - '@types/estree': 1.0.9 - '@types/json-schema': 7.0.15 - '@types/estree-jsx@1.0.0': dependencies: '@types/estree': 1.0.9 @@ -22965,8 +21904,6 @@ snapshots: '@types/estree@1.0.9': {} - '@types/eventsource@1.1.15': {} - '@types/express-serve-static-core@4.17.32': dependencies: '@types/node': 24.13.3 @@ -22982,10 +21919,6 @@ snapshots: '@types/geojson@7946.0.16': {} - '@types/gradient-string@1.1.2': - dependencies: - '@types/tinycolor2': 1.4.3 - '@types/hast@2.3.4': dependencies: '@types/unist': 3.0.3 @@ -23006,31 +21939,11 @@ snapshots: '@types/js-yaml@4.0.9': {} - '@types/json-query@2.2.3': {} - - '@types/json-schema@7.0.15': {} - - '@types/json5@0.0.29': {} - '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 0.7.31 '@types/node': 24.13.3 - '@types/keyv@3.1.4': - dependencies: - '@types/node': 24.13.3 - - '@types/lodash.get@4.4.9': - dependencies: - '@types/lodash': 4.14.191 - - '@types/lodash.omit@4.5.7': - dependencies: - '@types/lodash': 4.14.191 - - '@types/lodash@4.14.191': {} - '@types/marked@4.0.8': {} '@types/mdast@3.0.10': @@ -23064,11 +21977,6 @@ snapshots: '@types/node': 24.13.3 form-data: 4.0.6 - '@types/node-fetch@2.6.2': - dependencies: - '@types/node': 24.13.3 - form-data: 3.0.5 - '@types/node-fetch@2.6.4': dependencies: '@types/node': 24.13.3 @@ -23084,8 +21992,6 @@ snapshots: '@types/normalize-package-data@2.4.1': {} - '@types/object-hash@3.0.6': {} - '@types/pg-pool@2.0.6': dependencies: '@types/pg': 8.11.14 @@ -23114,12 +22020,6 @@ snapshots: dependencies: '@types/react': 18.2.69 - '@types/react@18.2.48': - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.2 - csstype: 3.1.1 - '@types/react@18.2.69': dependencies: '@types/prop-types': 15.7.5 @@ -23135,33 +22035,18 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/readable-stream@4.0.14': - dependencies: - '@types/node': 24.13.3 - safe-buffer: 5.1.2 - '@types/regression@2.0.6': {} '@types/resolve@1.20.6': {} - '@types/responselike@1.0.0': - dependencies: - '@types/node': 24.13.3 - '@types/retry@0.12.0': {} '@types/retry@0.12.2': {} - '@types/rimraf@4.0.5': - dependencies: - rimraf: 6.0.1 - '@types/scheduler@0.16.2': {} '@types/seedrandom@3.0.8': {} - '@types/semver@6.2.3': {} - '@types/semver@7.5.1': {} '@types/serve-static@1.15.0': @@ -23215,8 +22100,6 @@ snapshots: dependencies: '@types/node': 24.13.3 - '@types/tinycolor2@1.4.3': {} - '@types/trusted-types@2.0.7': optional: true @@ -23507,82 +22390,6 @@ snapshots: '@web3-storage/multipart-parser@1.0.0': {} - '@webassemblyjs/ast@1.14.1': - dependencies: - '@webassemblyjs/helper-numbers': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - - '@webassemblyjs/floating-point-hex-parser@1.13.2': {} - - '@webassemblyjs/helper-api-error@1.13.2': {} - - '@webassemblyjs/helper-buffer@1.14.1': {} - - '@webassemblyjs/helper-numbers@1.13.2': - dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.13.2 - '@webassemblyjs/helper-api-error': 1.13.2 - '@xtuc/long': 4.2.2 - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} - - '@webassemblyjs/helper-wasm-section@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/wasm-gen': 1.14.1 - - '@webassemblyjs/ieee754@1.13.2': - dependencies: - '@xtuc/ieee754': 1.2.0 - - '@webassemblyjs/leb128@1.13.2': - dependencies: - '@xtuc/long': 4.2.2 - - '@webassemblyjs/utf8@1.13.2': {} - - '@webassemblyjs/wasm-edit@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/helper-wasm-section': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-opt': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - '@webassemblyjs/wast-printer': 1.14.1 - - '@webassemblyjs/wasm-gen@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wasm-opt@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - - '@webassemblyjs/wasm-parser@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-api-error': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wast-printer@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@xtuc/long': 4.2.2 - '@window-splitter/interface@1.1.3': dependencies: '@window-splitter/state': 1.1.3(patch_hash=ecf02927f78361c14d8f8347604fd355ba36f2fc4f9ba9a08cd63adc101b7327) @@ -23605,10 +22412,6 @@ snapshots: '@xobotyi/scrollbar-width@1.9.5': {} - '@xtuc/ieee754@1.2.0': {} - - '@xtuc/long@4.2.2': {} - '@yuku-codegen/binding-darwin-arm64@0.7.2': optional: true @@ -23702,10 +22505,6 @@ snapshots: dependencies: acorn: 8.16.0 - acorn-import-phases@1.0.4(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -23733,11 +22532,6 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - aggregate-error@4.0.1: - dependencies: - clean-stack: 4.2.0 - indent-string: 5.0.0 - ahocorasick@1.0.2: {} ai@6.0.116(zod@3.25.76): @@ -23762,10 +22556,6 @@ snapshots: '@ai-sdk/provider-utils': 5.0.0-canary.44(zod@3.25.76) zod: 3.25.76 - ajv-formats@2.1.1(ajv@8.20.0): - optionalDependencies: - ajv: 8.20.0 - ajv-formats@3.0.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -23774,11 +22564,6 @@ snapshots: optionalDependencies: ajv: 8.20.0 - ajv-keywords@5.1.0(ajv@8.20.0): - dependencies: - ajv: 8.20.0 - fast-deep-equal: 3.1.3 - ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -23898,8 +22683,6 @@ snapshots: arrify@1.0.1: {} - arrify@3.0.0: {} - asap@2.0.6: {} asn1@0.2.6: @@ -23956,16 +22739,6 @@ snapshots: - encoding - ws - autoprefixer@10.4.13(postcss@8.5.26): - dependencies: - browserslist: 4.21.4 - caniuse-lite: 1.0.30001577 - fraction.js: 4.2.0 - normalize-range: 0.1.2 - picocolors: 1.0.0 - postcss: 8.5.26 - postcss-value-parser: 4.2.0 - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 @@ -24147,13 +22920,6 @@ snapshots: dependencies: pako: 0.2.9 - browserslist@4.21.4: - dependencies: - caniuse-lite: 1.0.30001577 - electron-to-chromium: 1.4.433 - node-releases: 2.0.12 - update-browserslist-db: 1.0.11(browserslist@4.21.4) - browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.10.11 @@ -24188,8 +22954,6 @@ snapshots: buildcheck@0.0.6: optional: true - builtins@1.0.3: {} - builtins@5.0.1: dependencies: semver: 7.8.5 @@ -24257,16 +23021,6 @@ snapshots: tar: 7.5.21 unique-filename: 3.0.0 - cacheable-request@6.1.0: - dependencies: - clone-response: 1.0.3 - get-stream: 5.2.0 - http-cache-semantics: 4.1.1 - keyv: 3.1.0 - lowercase-keys: 2.0.0 - normalize-url: 4.5.1 - responselike: 1.0.2 - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -24284,8 +23038,6 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - callsites@3.1.0: {} - camelcase-keys@6.2.2: dependencies: camelcase: 5.3.1 @@ -24294,8 +23046,6 @@ snapshots: camelcase@5.3.1: {} - caniuse-lite@1.0.30001577: {} - caniuse-lite@1.0.30001793: {} case-anything@2.1.13: {} @@ -24315,8 +23065,6 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.2.0: {} - chalk@5.3.0: {} chalk@5.6.2: {} @@ -24355,8 +23103,6 @@ snapshots: chownr@3.0.0: {} - chrome-trace-event@1.0.4: {} - ci-info@3.8.0: {} citty@0.1.6: @@ -24375,10 +23121,6 @@ snapshots: clean-stack@2.2.0: {} - clean-stack@4.2.0: - dependencies: - escape-string-regexp: 5.0.0 - cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -24426,10 +23168,6 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - clone-response@1.0.3: - dependencies: - mimic-response: 1.0.1 - clone@1.0.4: {} clsx@1.2.1: {} @@ -24610,43 +23348,12 @@ snapshots: dependencies: layout-base: 2.0.1 - cosmiconfig@9.0.0(typescript@7.0.2): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.0 - js-yaml: 4.3.1 - parse-json: 5.2.0 - optionalDependencies: - typescript: 7.0.2 - - cp-file@10.0.0: - dependencies: - graceful-fs: 4.2.11 - nested-error-stacks: 2.1.1 - p-event: 5.0.1 - cpu-features@0.0.10: dependencies: buildcheck: 0.0.6 nan: 2.23.1 optional: true - cpy-cli@5.0.0: - dependencies: - cpy: 10.1.0 - meow: 12.1.1 - - cpy@10.1.0: - dependencies: - arrify: 3.0.0 - cp-file: 10.0.0 - globby: 13.2.2 - junk: 4.0.1 - micromatch: 4.0.8 - nested-error-stacks: 2.1.1 - p-filter: 3.0.0 - p-map: 6.0.0 - crc-32@1.2.2: {} crc32-stream@6.0.0: @@ -24662,8 +23369,6 @@ snapshots: cronstrue@2.21.0: {} - cronstrue@2.61.0: {} - cross-env@7.0.3: dependencies: cross-spawn: 7.0.3 @@ -24694,19 +23399,6 @@ snapshots: dependencies: hyphenate-style-name: 1.0.4 - css-loader@6.10.0(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)): - dependencies: - icss-utils: 5.1.0(postcss@8.5.26) - postcss: 8.5.26 - postcss-modules-extract-imports: 3.0.0(postcss@8.5.26) - postcss-modules-local-by-default: 4.0.4(postcss@8.5.26) - postcss-modules-scope: 3.1.1(postcss@8.5.26) - postcss-modules-values: 4.0.0(postcss@8.5.26) - postcss-value-parser: 4.2.0 - semver: 7.8.1 - optionalDependencies: - webpack: 5.102.1(@swc/core@1.3.26)(esbuild@0.15.18) - css-tree@1.1.3: dependencies: mdn-data: 2.0.14 @@ -24721,8 +23413,6 @@ snapshots: cssesc@3.0.0: {} - csstype@3.1.1: {} - csstype@3.1.3: {} csstype@3.2.3: {} @@ -25001,16 +23691,13 @@ snapshots: dependencies: character-entities: 2.0.2 - decompress-response@3.3.0: - dependencies: - mimic-response: 1.0.1 - decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 optional: true - deep-extend@0.6.0: {} + deep-extend@0.6.0: + optional: true deep-object-diff@1.1.9: {} @@ -25029,8 +23716,6 @@ snapshots: dependencies: clone: 1.0.4 - defer-to-connect@1.1.3: {} - define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -25206,8 +23891,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - duplexer3@0.1.5: {} - duplexify@3.7.1: dependencies: end-of-stream: 1.4.5 @@ -25257,8 +23940,6 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 - electron-to-chromium@1.4.433: {} - electron-to-chromium@1.5.325: {} emoji-regex@8.0.0: {} @@ -25278,6 +23959,7 @@ snapshots: encoding@0.1.13: dependencies: iconv-lite: 0.6.3 + optional: true end-of-stream@1.4.4: dependencies: @@ -25331,8 +24013,6 @@ snapshots: entities@6.0.1: {} - env-paths@2.2.1: {} - env-paths@3.0.0: {} environment@1.1.0: {} @@ -25718,21 +24398,8 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-scope@5.1.1: - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - esprima@4.0.1: {} - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@4.3.0: {} - - estraverse@5.3.0: {} - estree-util-attach-comments@2.1.0: dependencies: '@types/estree': 1.0.9 @@ -25799,16 +24466,12 @@ snapshots: event-target-shim@5.0.1: {} - event-target-shim@6.0.2: {} - eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} events@3.3.0: {} - eventsource-parser@1.1.2: {} - eventsource-parser@3.0.0: {} eventsource-parser@3.0.6: {} @@ -26242,8 +24905,6 @@ snapshots: forwarded@0.2.0: {} - fraction.js@4.2.0: {} - framer-motion@10.12.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: tslib: 2.5.0 @@ -26329,14 +24990,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@4.1.0: - dependencies: - pump: 3.0.4 - - get-stream@5.2.0: - dependencies: - pump: 3.0.4 - get-stream@6.0.1: {} get-stream@8.0.1: {} @@ -26393,8 +25046,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-to-regexp@0.4.1: {} - glob@10.4.5: dependencies: foreground-child: 3.3.1 @@ -26436,41 +25087,12 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - globby@13.2.2: - dependencies: - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 4.0.0 - globrex@0.1.2: {} gopd@1.2.0: {} - got@9.6.0: - dependencies: - '@sindresorhus/is': 0.14.0 - '@szmarczak/http-timer': 1.1.2 - '@types/keyv': 3.1.4 - '@types/responselike': 1.0.0 - cacheable-request: 6.1.0 - decompress-response: 3.3.0 - duplexer3: 0.1.5 - get-stream: 4.1.0 - lowercase-keys: 1.0.1 - mimic-response: 1.0.1 - p-cancelable: 1.1.0 - to-readable-stream: 1.0.0 - url-parse-lax: 3.0.0 - graceful-fs@4.2.11: {} - gradient-string@2.0.2: - dependencies: - chalk: 4.1.2 - tinygradient: 1.1.5 - grapheme-splitter@1.0.4: {} graphql@16.14.2: {} @@ -26670,8 +25292,6 @@ snapshots: domutils: 3.0.1 entities: 4.5.0 - http-cache-semantics@4.1.1: {} - http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -26733,11 +25353,6 @@ snapshots: ignore@7.0.5: {} - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - import-in-the-middle@1.15.0: dependencies: acorn: 8.16.0 @@ -26752,16 +25367,12 @@ snapshots: cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 - import-meta-resolve@4.1.0: {} - import-without-cache@0.4.0: {} imurmurhash@0.1.4: {} indent-string@4.0.0: {} - indent-string@5.0.0: {} - inherits@2.0.4: {} ini@1.3.8: {} @@ -27017,14 +25628,6 @@ snapshots: javascript-stringify@2.1.0: {} - jest-worker@27.5.1: - dependencies: - '@types/node': 24.13.3 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - jiti@1.21.0: {} - jiti@1.21.6: {} jiti@2.4.2: {} @@ -27068,8 +25671,6 @@ snapshots: jsesc@3.0.2: {} - json-buffer@3.0.0: {} - json-parse-even-better-errors@2.3.1: {} json-parse-even-better-errors@3.0.0: {} @@ -27092,10 +25693,6 @@ snapshots: jsonify: 0.0.1 object-keys: 1.1.1 - json5@1.0.2: - dependencies: - minimist: 1.2.7 - json5@2.2.3: {} jsonc-parser@3.2.1: {} @@ -27122,8 +25719,6 @@ snapshots: '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) jsep: 1.4.0 - jsonpointer@5.0.1: {} - jsonwebtoken@9.0.2: dependencies: jws: 3.2.3 @@ -27137,8 +25732,6 @@ snapshots: ms: 2.1.3 semver: 7.8.5 - junk@4.0.1: {} - jwa@1.4.2: dependencies: buffer-equal-constant-time: 1.0.1 @@ -27156,10 +25749,6 @@ snapshots: dependencies: commander: 8.3.0 - keyv@3.1.0: - dependencies: - json-buffer: 3.0.0 - khroma@2.1.0: {} kind-of@6.0.3: {} @@ -27312,8 +25901,6 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 - loader-runner@4.3.1: {} - loader-utils@3.2.1: {} local-pkg@0.4.3: {} @@ -27388,10 +25975,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - lowercase-keys@1.0.1: {} - - lowercase-keys@2.0.0: {} - lru-cache@10.4.3: {} lru-cache@11.2.4: {} @@ -27743,8 +26326,6 @@ snapshots: media-typer@1.1.0: {} - meow@12.1.1: {} - meow@6.1.1: dependencies: '@types/minimist': 1.2.2 @@ -28224,8 +26805,6 @@ snapshots: mimic-function@5.0.1: {} - mimic-response@1.0.1: {} - mimic-response@3.1.0: optional: true @@ -28263,8 +26842,6 @@ snapshots: is-plain-obj: 1.1.0 kind-of: 6.0.3 - minimist@1.2.7: {} - minimist@1.2.8: {} minipass-collect@1.0.2: @@ -28423,10 +27000,6 @@ snapshots: negotiator@1.0.0: {} - neo-async@2.6.2: {} - - nested-error-stacks@2.1.1: {} - neverthrow@8.2.0: optionalDependencies: '@rollup/rollup-linux-x64-gnu': 4.60.1 @@ -28455,17 +27028,9 @@ snapshots: optionalDependencies: encoding: 0.1.13 - node-fetch@2.6.7(encoding@0.1.13): - dependencies: - whatwg-url: 5.0.0 - optionalDependencies: - encoding: 0.1.13 - node-gyp-build@4.8.4: optional: true - node-releases@2.0.12: {} - node-releases@2.0.36: {} nodemailer@9.0.3: {} @@ -28492,10 +27057,6 @@ snapshots: normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - - normalize-url@4.5.1: {} - notepack.io@3.0.1: {} npm-install-checks@6.2.0: @@ -28567,8 +27128,6 @@ snapshots: object-assign@4.1.1: {} - object-hash@3.0.0: {} - object-inspect@1.13.4: {} object-is@1.1.6: @@ -28797,20 +27356,10 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.70.0 '@oxlint/binding-win32-x64-msvc': 1.70.0 - p-cancelable@1.1.0: {} - - p-event@5.0.1: - dependencies: - p-timeout: 5.1.0 - p-filter@2.1.0: dependencies: p-map: 2.1.0 - p-filter@3.0.0: - dependencies: - p-map: 5.5.0 - p-finally@1.0.0: {} p-limit@2.3.0: @@ -28847,10 +27396,6 @@ snapshots: dependencies: aggregate-error: 3.1.0 - p-map@5.5.0: - dependencies: - aggregate-error: 4.0.1 - p-map@6.0.0: {} p-queue@6.6.2: @@ -28873,27 +27418,14 @@ snapshots: dependencies: p-finally: 1.0.0 - p-timeout@5.1.0: {} - p-try@2.2.0: {} package-json-from-dist@1.0.1: {} - package-json@6.5.0: - dependencies: - got: 9.6.0 - registry-auth-token: 4.2.2 - registry-url: 5.1.0 - semver: 6.3.1 - package-manager-detector@1.4.1: {} pako@0.2.9: {} - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - parse-duration@2.1.4: {} parse-entities@4.0.0: @@ -28907,8 +27439,6 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 - parse-github-url@1.0.2: {} - parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.7 @@ -28939,10 +27469,6 @@ snapshots: parseurl@1.3.3: {} - partysocket@1.0.2: - dependencies: - event-target-shim: 6.0.2 - path-data-parser@0.1.0: {} path-exists@4.0.0: {} @@ -29042,8 +27568,6 @@ snapshots: dependencies: split2: 4.2.0 - picocolors@1.0.0: {} - picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -29056,8 +27580,6 @@ snapshots: pidtree@0.6.0: {} - pify@2.3.0: {} - pify@4.0.1: {} pino-abstract-transport@3.0.0: @@ -29123,13 +27645,6 @@ snapshots: dependencies: postcss: 8.5.26 - postcss-import@16.0.1(postcss@8.5.26): - dependencies: - postcss: 8.5.26 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.8 - postcss-load-config@4.0.2(postcss@8.5.26): dependencies: lilconfig: 3.1.3 @@ -29137,17 +27652,6 @@ snapshots: optionalDependencies: postcss: 8.5.26 - postcss-loader@8.1.1(postcss@8.5.26)(typescript@7.0.2)(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)): - dependencies: - cosmiconfig: 9.0.0(typescript@7.0.2) - jiti: 1.21.0 - postcss: 8.5.26 - semver: 7.8.1 - optionalDependencies: - webpack: 5.102.1(@swc/core@1.3.26)(esbuild@0.15.18) - transitivePeerDependencies: - - typescript - postcss-modules-extract-imports@3.0.0(postcss@8.5.26): dependencies: postcss: 8.5.26 @@ -29256,8 +27760,6 @@ snapshots: path-exists: 4.0.0 which-pm: 2.0.0 - prepend-http@2.0.0: {} - prettier@2.8.8: {} prettier@3.8.3: {} @@ -29460,6 +27962,7 @@ snapshots: ini: 1.3.8 minimist: 1.2.8 strip-json-comments: 2.0.1 + optional: true react-aria@3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -29684,10 +28187,6 @@ snapshots: react@19.0.0-rc.1: {} - read-cache@1.0.0: - dependencies: - pify: 2.3.0 - read-pkg-up@7.0.1: dependencies: find-up: 4.1.0 @@ -29807,14 +28306,6 @@ snapshots: reghex@3.0.2: {} - registry-auth-token@4.2.2: - dependencies: - rc: 1.2.8 - - registry-url@5.1.0: - dependencies: - rc: 1.2.8 - regression@2.0.1: {} rehype-harden@1.1.8: @@ -29997,8 +28488,6 @@ snapshots: resize-observer-polyfill@1.5.1: {} - resolve-from@4.0.0: {} - resolve-from@5.0.0: {} resolve-import@2.4.0: @@ -30016,10 +28505,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - responselike@1.0.2: - dependencies: - lowercase-keys: 1.0.1 - restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -30202,17 +28687,8 @@ snapshots: scheduler@0.25.0-rc.1: {} - schema-utils@4.3.3: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 8.20.0 - ajv-formats: 2.1.1(ajv@8.20.0) - ajv-keywords: 5.1.0(ajv@8.20.0) - screenfull@5.2.0: {} - secure-json-parse@2.7.0: {} - secure-json-parse@4.0.0: {} seedrandom@3.0.5: {} @@ -30221,11 +28697,6 @@ snapshots: dependencies: parseley: 0.12.1 - sembear@0.5.2: - dependencies: - '@types/semver': 6.2.3 - semver: 6.3.1 - semver@5.7.2: {} semver@6.3.1: {} @@ -30435,8 +28906,6 @@ snapshots: slash@3.0.0: {} - slash@4.0.0: {} - slice-ansi@4.0.0: dependencies: ansi-styles: 4.3.0 @@ -30711,7 +29180,8 @@ snapshots: dependencies: min-indent: 1.0.1 - strip-json-comments@2.0.1: {} + strip-json-comments@2.0.1: + optional: true strip-json-comments@5.0.3: {} @@ -30730,10 +29200,6 @@ snapshots: stubborn-utils@1.0.2: {} - style-loader@3.3.4(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)): - dependencies: - webpack: 5.102.1(@swc/core@1.3.26)(esbuild@0.15.18) - style-mod@4.1.3: {} style-to-js@1.1.16: @@ -30789,10 +29255,6 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - supports-hyperlinks@3.1.0: dependencies: has-flag: 4.0.0 @@ -30897,23 +29359,13 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.4.0(@swc/core@1.3.26)(esbuild@0.15.18)(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - terser: 5.46.1 - webpack: 5.102.1(@swc/core@1.3.26)(esbuild@0.15.18) - optionalDependencies: - '@swc/core': 1.3.26 - esbuild: 0.15.18 - terser@5.46.1: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 + optional: true testcontainers@11.14.0: dependencies: @@ -30968,8 +29420,6 @@ snapshots: tinybench@2.9.0: {} - tinycolor2@1.6.0: {} - tinyexec@0.3.0: {} tinyexec@0.3.1: {} @@ -30993,11 +29443,6 @@ snapshots: fdir: 6.2.0(picomatch@4.0.4) picomatch: 4.0.4 - tinygradient@1.1.5: - dependencies: - '@types/tinycolor2': 1.4.3 - tinycolor2: 1.6.0 - tinypool@2.1.0: {} tinyrainbow@3.1.0: {} @@ -31016,8 +29461,6 @@ snapshots: to-fast-properties@2.0.0: {} - to-readable-stream@1.0.0: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -31075,10 +29518,6 @@ snapshots: tsafe@1.4.1: {} - tsconfck@2.1.2(typescript@7.0.2): - optionalDependencies: - typescript: 7.0.2 - tsconfck@3.1.3(typescript@6.0.3): optionalDependencies: typescript: 6.0.3 @@ -31087,13 +29526,6 @@ snapshots: optionalDependencies: typescript: 7.0.2 - tsconfig-paths@3.14.1: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.7 - strip-bom: 3.0.0 - tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 @@ -31170,8 +29602,6 @@ snapshots: typescript: 6.0.3 walk-up-path: 4.0.0 - tslib@2.4.1: {} - tslib@2.5.0: {} tslib@2.6.2: {} @@ -31317,10 +29747,6 @@ snapshots: is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 - typed-emitter@2.1.0: - optionalDependencies: - rxjs: 7.8.2 - typescript@5.6.1-rc: {} typescript@5.9.3: {} @@ -31360,8 +29786,6 @@ snapshots: uint8array-extras@1.5.0: {} - ulid@2.3.0: {} - unbash@4.0.2: {} unbox-primitive@1.0.2: @@ -31490,22 +29914,12 @@ snapshots: unpipe@1.0.0: {} - update-browserslist-db@1.0.11(browserslist@4.21.4): - dependencies: - browserslist: 4.21.4 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 - url-parse-lax@3.0.0: - dependencies: - prepend-http: 2.0.0 - use-callback-ref@1.3.3(@types/react@18.2.69)(react@18.3.1): dependencies: react: 18.3.1 @@ -31575,10 +29989,6 @@ snapshots: spdx-correct: 3.1.1 spdx-expression-parse: 3.0.1 - validate-npm-package-name@3.0.0: - dependencies: - builtins: 1.0.3 - validate-npm-package-name@5.0.0: dependencies: builtins: 5.0.1 @@ -31679,15 +30089,6 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@4.0.5(typescript@7.0.2): - dependencies: - debug: 4.3.7(supports-color@10.0.0) - globrex: 0.1.2 - tsconfck: 2.1.2(typescript@7.0.2) - transitivePeerDependencies: - - supports-color - - typescript - vite-tsconfig-paths@5.1.4(typescript@7.0.2)(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0)): dependencies: debug: 4.4.3(supports-color@10.0.0) @@ -31827,11 +30228,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - watchpack@2.5.1: - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -31850,40 +30246,6 @@ snapshots: webidl-conversions@3.0.1: {} - webpack-sources@3.3.4: {} - - webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.9 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.16.0 - acorn-import-phases: 1.0.4(acorn@8.16.0) - browserslist: 4.28.1 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.21.6 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.3 - terser-webpack-plugin: 5.4.0(@swc/core@1.3.26)(esbuild@0.15.18)(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)) - watchpack: 2.5.1 - webpack-sources: 3.3.4 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - whatwg-url@5.0.0: dependencies: tr46: 0.0.3 From e768d0a724332a916582cc4ced689923db0156e2 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Tue, 18 Aug 2026 13:14:01 +0200 Subject: [PATCH 05/98] feat(webapp): run the dashboard agent through AWS Bedrock behind an env switch (#4609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What & why The dashboard agent can now run its model calls through AWS Bedrock instead of the direct Anthropic API, chosen by a single env switch. It's **off by default** (`DASHBOARD_AGENT_MODEL_PROVIDER` unset ⇒ `anthropic`), so merging changes nothing at runtime — the Bedrock path is a dormant branch until an operator sets the switch and AWS config. The default Anthropic path is byte-for-byte unchanged. This also carries a related tenant-isolation hardening for the agent's delegated token (kept together deliberately — both land the agent on Bedrock for HIPAA readiness). Refs: TRI-13251, TRI-11032. ## What's inside **Provider seam** — `internal-packages/dashboard-agent/src/model-provider.ts`: the registry now holds both `anthropic` and `bedrock`; `resolveDashboardAgentModel()` maps the canonical `"anthropic:"` strings the managed prompts carry to the active provider, and the cache-breakpoint helpers emit the active provider's shape — Anthropic `cacheControl` vs Bedrock `cachePoint`. Managed prompt strings stay canonical, so stored prompts don't change meaning. Unmapped model ids throw rather than shipping a guaranteed-404 profile. All agent, watch, compaction and title callsites route through the resolver; the `dashboardAgentModelKey` locals override (test mock injection) is preserved. **Cache telemetry** — `step-cache.ts`: cache token usage is read from the active provider (Anthropic reports it on provider metadata; Bedrock reports the write on metadata and the read via standard usage), so `gen_ai.usage.cache_*` is populated on both. This also fixes a latent ordering bug where step attributes could null-overwrite the prompt-cache read count. **Webapp callsites** — `dashboardAgentHeadStart.server.ts` and the head-start route resolve the model and the cache breakpoint through the same seam, so the warm-up prefix and the following turn share one provider. The head-start firing gate is provider-aware: on Bedrock it gates on `AWS_REGION` and lets the SDK resolve credentials (IAM role / static keys / session token / bearer), so a role-based deploy still warms; on Anthropic it stays `Boolean(ANTHROPIC_API_KEY)`. `app/env.server.ts` gains the optional AWS vars and validates `DASHBOARD_AGENT_MODEL_PROVIDER`. `ANTHROPIC_API_KEY` is untouched and not required on a Bedrock deploy. **Tenant-isolation hardening** — `internal-packages/rbac/src/fallback.ts`: for a **scoped** context, the OSS `authenticateUserActor` now applies the same membership floor as the session path — a delegated user-actor token whose user is not a member of the scoped org/project is denied (403). Unscoped tokens keep their prior behavior (no tenant claim, no lookup). The user lookup falls back replica→primary so replication lag can't spuriously 401 a just-joined member. Members and admins are unaffected. Previously this invariant held only through per-route discipline; this makes it structural. ## Enabling Bedrock (later, ops) - Set `DASHBOARD_AGENT_MODEL_PROVIDER=bedrock` **identically** in both the webapp and the agent task container — the webapp warms the cache prefix and the task reads it, so a split would silently miss the cache. - Set `AWS_REGION` and provide credentials the Bedrock SDK can resolve (IAM role preferred). For v1 this runs **without** an Anthropic API key. Note: with no Anthropic key set, rollback is "turn the agent off", not "unset the switch" (unsetting falls back to the Anthropic provider, which then has no key). - Two things to confirm before rollout: the Sonnet inference-profile id is validated against the SDK's own model-id union but still warrants a live smoke test; and Bedrock prompt caching for Sonnet is a 5-minute window (not Anthropic's 1h), so input-token cost rises when flipped. ## Testing Unit tests cover both provider paths: the provider switch and per-provider cache shapes, a structural regex asserting Bedrock ids are real inference profiles (not an echo of the table), the split-metadata cache telemetry, and real-Postgres RBAC tests — member allowed, scoped non-member denied (org-only and project-only), missing user → 401, admin non-member exempt, unscoped success. `typecheck --filter webapp` and the dashboard-agent + rbac suites pass. --- apps/webapp/app/env.server.ts | 22 +++ ...jectParam.env.$envParam.dashboard-agent.ts | 6 +- .../dashboardAgentHeadStart.server.ts | 14 +- .../dashboard-agent/package.json | 4 +- .../dashboard-agent/src/agent-runtime.ts | 26 +-- .../src/cache-breakpoint.test.ts | 2 + .../dashboard-agent/src/compaction.ts | 4 +- .../dashboard-agent/src/dashboard-agent.ts | 14 +- .../dashboard-agent/src/eval-turn.ts | 4 +- .../src/model-provider.test.ts | 184 ++++++++++++++++++ .../dashboard-agent/src/model-provider.ts | 181 +++++++++++++++++ .../dashboard-agent/src/step-cache.test.ts | 109 ++++++++++- .../dashboard-agent/src/step-cache.ts | 60 ++---- .../dashboard-agent/src/watch-actions.ts | 12 +- internal-packages/rbac/package.json | 3 +- internal-packages/rbac/src/fallback.ts | 23 +++ .../rbac/src/fallback.userActor.test.ts | 102 ++++++++++ internal-packages/rbac/tsconfig.json | 4 +- internal-packages/rbac/tsconfig.src.json | 19 ++ internal-packages/rbac/tsconfig.test.json | 18 ++ pnpm-lock.yaml | 44 ++++- 21 files changed, 761 insertions(+), 94 deletions(-) create mode 100644 internal-packages/dashboard-agent/src/model-provider.test.ts create mode 100644 internal-packages/dashboard-agent/src/model-provider.ts create mode 100644 internal-packages/rbac/src/fallback.userActor.test.ts create mode 100644 internal-packages/rbac/tsconfig.src.json create mode 100644 internal-packages/rbac/tsconfig.test.json diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 7a6c4c8aea1..1d6000caa24 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -209,6 +209,28 @@ const EnvironmentSchema = z // uses its own key on the Trigger side. When unset, Head Start is disabled // and the first turn falls back to the normal cold-start path. ANTHROPIC_API_KEY: z.string().optional(), + // Selects the dashboard agent's LLM provider (default anthropic). The internal + // seam reads process.env directly; this entry validates the value webapp-side. + DASHBOARD_AGENT_MODEL_PROVIDER: z.preprocess( + (v) => (typeof v === "string" && v.trim() === "" ? undefined : v), + z.enum(["anthropic", "bedrock"]).default("anthropic") + ), + // AWS credentials for the dashboard agent's Bedrock provider (only used when + // DASHBOARD_AGENT_MODEL_PROVIDER=bedrock; default path stays Anthropic). The + // provider resolves credentials itself, so only the region is read here. + AWS_REGION: z.string().optional(), + AWS_DEFAULT_REGION: z.string().optional(), + AWS_ACCESS_KEY_ID: z.string().optional(), + AWS_SECRET_ACCESS_KEY: z.string().optional(), + AWS_SESSION_TOKEN: z.string().optional(), + AWS_BEARER_TOKEN_BEDROCK: z.string().optional(), + // Dedicated, non-global credentials for the dashboard agent's Bedrock calls (a + // Bedrock-invoke-only IAM user). Kept separate from AWS_ACCESS_KEY_ID/etc so + // injecting them can't hijack the default credential chain the ECR/STS deploy + // clients rely on. + DASHBOARD_AGENT_AWS_ACCESS_KEY_ID: z.string().optional(), + DASHBOARD_AGENT_AWS_SECRET_ACCESS_KEY: z.string().optional(), + DASHBOARD_AGENT_AWS_REGION: z.string().optional(), DIRECT_URL: z .string() .refine( diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.ts index 499e621b83d..2f8d6fb6166 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.ts @@ -17,6 +17,7 @@ import { softDeleteChat, } from "@internal/dashboard-agent-db"; import { watchDraftSchema, type WatchDraft } from "@internal/dashboard-agent-contracts"; +import { dashboardAgentProvider } from "@internal/dashboard-agent/model-provider"; import { generateFriendlyId } from "@trigger.dev/core/v3/isomorphic"; import type { UIMessage } from "ai"; import { z } from "zod"; @@ -329,7 +330,10 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const chatId = generateFriendlyId("chat"); try { const repoSnapshot = await resolveDashboardAgentRepoSnapshot(project.id); - const headStarted = Boolean(env.ANTHROPIC_API_KEY); + const headStarted = + dashboardAgentProvider() === "bedrock" + ? Boolean(env.DASHBOARD_AGENT_AWS_REGION || env.AWS_REGION || env.AWS_DEFAULT_REGION) + : Boolean(env.ANTHROPIC_API_KEY); // The lookups and the mint all run before the chat row exists, so a failure here can't // leave an empty chat behind in the user's history. diff --git a/apps/webapp/app/services/dashboardAgentHeadStart.server.ts b/apps/webapp/app/services/dashboardAgentHeadStart.server.ts index 4a5936a196b..1cb8cae34d3 100644 --- a/apps/webapp/app/services/dashboardAgentHeadStart.server.ts +++ b/apps/webapp/app/services/dashboardAgentHeadStart.server.ts @@ -1,4 +1,3 @@ -import { createAnthropic } from "@ai-sdk/anthropic"; import { DASHBOARD_AGENT_CODE_SYSTEM_PROMPT, DASHBOARD_AGENT_MODEL, @@ -8,9 +7,12 @@ import { } from "@internal/dashboard-agent/tool-schemas"; import { describePromptPrefix, - PROMPT_CACHE_CONTROL, promptCacheAttributes, } from "@internal/dashboard-agent/prompt-prefix"; +import { + resolveDashboardAgentModel, + withCacheBreakpoint, +} from "@internal/dashboard-agent/model-provider"; import { ApiClient, SessionStreamInstance, writeTurnCompleteRecord } from "@trigger.dev/core/v3"; import { chat as chatServer } from "@trigger.dev/sdk/chat-server"; import { streamText, type UIMessage, type UIMessageChunk } from "ai"; @@ -23,8 +25,6 @@ import { logger } from "~/services/logger.server"; const TASK_ID = "dashboard-agent"; -const anthropic = createAnthropic({ apiKey: env.ANTHROPIC_API_KEY }); - /** Shown when the warm first turn produced nothing. The provider error is only logged. */ export const HEAD_START_FAILURE_ERROR_TEXT = "The assistant couldn't start this response. Please send your message again."; @@ -113,16 +113,16 @@ export async function startDashboardAgentHeadStart(params: { run: async ({ chat: helper }) => streamText({ ...helper.toStreamTextOptions({ tools }), - model: anthropic(DASHBOARD_AGENT_MODEL), + model: resolveDashboardAgentModel(DASHBOARD_AGENT_MODEL), // A structured system message, not a bare string: without provider options - // Anthropic neither writes nor reads the cache, so this call paid full price + // the provider neither writes nor reads the cache, so this call paid full price // for the prefix and the agent's step 2 then paid for a fresh write. The tool // key order is frozen (see `tool-schemas.ts`) so both prefixes are identical // — the logged fingerprint is how a drift becomes visible. system: { role: "system", content: system, - providerOptions: { anthropic: { cacheControl: PROMPT_CACHE_CONTROL } }, + providerOptions: withCacheBreakpoint(undefined, "prefix"), }, onStepFinish: (step) => { logger.info( diff --git a/internal-packages/dashboard-agent/package.json b/internal-packages/dashboard-agent/package.json index 1808d60e777..eaf9747b212 100644 --- a/internal-packages/dashboard-agent/package.json +++ b/internal-packages/dashboard-agent/package.json @@ -9,9 +9,11 @@ ".": "./src/index.ts", "./tool-curation": "./src/tool-curation.ts", "./tool-schemas": "./src/tool-schemas.ts", - "./prompt-prefix": "./src/prompt-prefix.ts" + "./prompt-prefix": "./src/prompt-prefix.ts", + "./model-provider": "./src/model-provider.ts" }, "dependencies": { + "@ai-sdk/amazon-bedrock": "4.0.117", "@ai-sdk/anthropic": "^3.0.0", "@internal/dashboard-agent-contracts": "workspace:*", "@internal/dashboard-agent-db": "workspace:*", diff --git a/internal-packages/dashboard-agent/src/agent-runtime.ts b/internal-packages/dashboard-agent/src/agent-runtime.ts index d8d20ba2e20..fe1d99e3699 100644 --- a/internal-packages/dashboard-agent/src/agent-runtime.ts +++ b/internal-packages/dashboard-agent/src/agent-runtime.ts @@ -1,4 +1,3 @@ -import { anthropic } from "@ai-sdk/anthropic"; import { appendChatMessageOnce, createDashboardAgentDb, @@ -18,13 +17,7 @@ import { type UpsertInvestigationResult, } from "@internal/dashboard-agent-db"; import { locals, logger } from "@trigger.dev/sdk"; -import { - createProviderRegistry, - type LanguageModel, - type ModelMessage, - type ToolSet, - type UIMessage, -} from "ai"; +import { type LanguageModel, type ModelMessage, type ToolSet, type UIMessage } from "ai"; import { z } from "zod"; import { agentPageContextSchema, @@ -32,8 +25,8 @@ import { investigationStateSchema, type InvestigationState, } from "@internal/dashboard-agent-contracts"; +import { withCacheBreakpoint } from "./model-provider"; import { codeSystemPrompt, systemPrompt } from "./prompts"; -import { PROMPT_CACHE_CONTROL } from "./prompt-prefix"; import { buildDashboardAgentTools } from "./tools"; /** @@ -63,8 +56,8 @@ function getDb(): DashboardAgentDbClient { } // Resolves the `"provider:model-id"` strings on our managed prompts to AI SDK -// models. Add another @ai-sdk/* provider here to allow it on a prompt. -export const registry = createProviderRegistry({ anthropic }); +// models, against whichever provider is switched on. +export { registry, resolveDashboardAgentModel } from "./model-provider"; // The agent's persistence, behind an interface so tests can inject a fake via // `locals` and never need a real database. @@ -354,7 +347,7 @@ export function sanitizeReplayedToolInputs(messages: ModelMessage[]): ModelMessa }) as ModelMessage[]; } -// Same Anthropic breakpoint `prepareMessages` rolls onto a turn's last message. +// Same breakpoint `prepareMessages` rolls onto a turn's last message. export function withCacheBreakpointOnLast(messages: ModelMessage[]): ModelMessage[] { if (messages.length === 0) return messages; const last = messages[messages.length - 1]!; @@ -362,12 +355,9 @@ export function withCacheBreakpointOnLast(messages: ModelMessage[]): ModelMessag ...messages.slice(0, -1), { ...last, - providerOptions: { - ...last.providerOptions, - // Merged, not replaced: the breakpoint is one Anthropic option among any - // others the message already carries. - anthropic: { ...last.providerOptions?.anthropic, cacheControl: PROMPT_CACHE_CONTROL }, - }, + // Merged, not replaced: the breakpoint is one provider option among any + // others the message already carries. + providerOptions: withCacheBreakpoint(last.providerOptions, "prefix"), }, ]; } diff --git a/internal-packages/dashboard-agent/src/cache-breakpoint.test.ts b/internal-packages/dashboard-agent/src/cache-breakpoint.test.ts index a3b64b20826..483789b02e8 100644 --- a/internal-packages/dashboard-agent/src/cache-breakpoint.test.ts +++ b/internal-packages/dashboard-agent/src/cache-breakpoint.test.ts @@ -35,6 +35,7 @@ describe("withCacheBreakpointOnLast", () => { const prepared = withCacheBreakpointOnLast(lastMessageWithAnthropicOptions()); expect(prepared[1]!.providerOptions).toEqual({ + __cacheBreakpoint: { kind: "prefix" }, anthropic: { cacheControl: PROMPT_CACHE_CONTROL, thinking: { budget: 1024 } }, openai: { store: false }, }); @@ -54,6 +55,7 @@ describe("prepareTurnMessages", () => { }); expect(prepared[1]!.providerOptions).toEqual({ + __cacheBreakpoint: { kind: "prefix" }, anthropic: { cacheControl: PROMPT_CACHE_CONTROL, thinking: { budget: 1024 } }, openai: { store: false }, }); diff --git a/internal-packages/dashboard-agent/src/compaction.ts b/internal-packages/dashboard-agent/src/compaction.ts index 68001b00ba4..b8bed58f599 100644 --- a/internal-packages/dashboard-agent/src/compaction.ts +++ b/internal-packages/dashboard-agent/src/compaction.ts @@ -5,7 +5,7 @@ import { generateText, type ModelMessage, type UIMessage } from "ai"; import { dashboardAgentModelKey, latestCards, - registry, + resolveDashboardAgentModel, sanitizeReplayedToolInputs, } from "./agent-runtime"; @@ -271,7 +271,7 @@ export function renderTranscriptForSummary(messages: ModelMessage[]): string { async function summarizeConversation(event: SummarizeEvent): Promise { const { text } = await generateText({ - model: locals.get(dashboardAgentModelKey) ?? registry.languageModel(SUMMARY_MODEL), + model: locals.get(dashboardAgentModelKey) ?? resolveDashboardAgentModel(SUMMARY_MODEL), system: SUMMARY_INSTRUCTION, prompt: renderTranscriptForSummary(event.messages), maxOutputTokens: SUMMARY_MAX_OUTPUT_TOKENS, diff --git a/internal-packages/dashboard-agent/src/dashboard-agent.ts b/internal-packages/dashboard-agent/src/dashboard-agent.ts index 237cbc64d3c..c47da63918d 100644 --- a/internal-packages/dashboard-agent/src/dashboard-agent.ts +++ b/internal-packages/dashboard-agent/src/dashboard-agent.ts @@ -16,7 +16,7 @@ import { getStore, getSystemPrompt, modeFor, - registry, + resolveDashboardAgentModel, sanitizeReplayedToolInputs, settlementCardMessages, clearOpenInvestigations, @@ -25,7 +25,7 @@ import { type DashboardAgentStore, } from "./agent-runtime"; import { titlePrompt } from "./prompts"; -import { PROMPT_CACHE_CONTROL } from "./prompt-prefix"; +import { withCacheBreakpoint } from "./model-provider"; import { recordPromptCacheUsage, stepCachePrepareStep } from "./step-cache"; import { dashboardAgentActionSchema, handleWatchAction } from "./watch-actions"; import { dashboardAgentCompaction, withDurableState } from "./compaction"; @@ -309,9 +309,7 @@ async function generateAndSaveTitle( const { text } = await generateText({ model: locals.get(dashboardAgentModelKey) ?? - registry.languageModel( - (resolved.model ?? "anthropic:claude-haiku-4-5") as `anthropic:${string}` - ), + resolveDashboardAgentModel(resolved.model ?? "anthropic:claude-haiku-4-5"), system: resolved.text, prompt: userText, ...resolved.toAISDKTelemetry(), @@ -428,7 +426,7 @@ export const dashboardAgent = chat.agent({ // prompt; the resolve is cached per process. The cache breakpoint on the system // block carries through toStreamTextOptions() and survives suspend/resume. chat.prompt.set(await getSystemPrompt(modeFor(clientData)), { - providerOptions: { anthropic: { cacheControl: PROMPT_CACHE_CONTROL } }, + providerOptions: withCacheBreakpoint(undefined, "prefix"), }); }, @@ -581,9 +579,7 @@ export const dashboardAgent = chat.agent({ ...options, model: locals.get(dashboardAgentModelKey) ?? - registry.languageModel( - (resolved.model ?? "anthropic:claude-sonnet-4-6") as `anthropic:${string}` - ), + resolveDashboardAgentModel(resolved.model ?? "anthropic:claude-sonnet-4-6"), messages, abortSignal: signal, prepareStep: stepCachePrepareStep(options) as never, diff --git a/internal-packages/dashboard-agent/src/eval-turn.ts b/internal-packages/dashboard-agent/src/eval-turn.ts index 3327611e4d0..b3e55ddfc6e 100644 --- a/internal-packages/dashboard-agent/src/eval-turn.ts +++ b/internal-packages/dashboard-agent/src/eval-turn.ts @@ -1,4 +1,3 @@ -import { anthropic } from "@ai-sdk/anthropic"; import { createDashboardAgentDb, insertTurnEval, @@ -6,6 +5,7 @@ import { } from "@internal/dashboard-agent-db"; import { logger, task } from "@trigger.dev/sdk"; import { EVAL_ERROR_CATEGORIES, redactedEvalOutputErrored } from "./eval-policy"; +import { resolveDashboardAgentModel } from "./model-provider"; import { generateObject } from "ai"; import { z } from "zod"; @@ -164,7 +164,7 @@ export const evalTurn = task({ id: "dashboard-agent-eval-turn", run: async (payload: EvalTurnPayload, { ctx }) => { const { object } = await generateObject({ - model: anthropic(JUDGE_MODEL), + model: resolveDashboardAgentModel(`anthropic:${JUDGE_MODEL}`), schema: TurnEval, system: JUDGE_SYSTEM, prompt: [ diff --git a/internal-packages/dashboard-agent/src/model-provider.test.ts b/internal-packages/dashboard-agent/src/model-provider.test.ts new file mode 100644 index 00000000000..c238a6199ca --- /dev/null +++ b/internal-packages/dashboard-agent/src/model-provider.test.ts @@ -0,0 +1,184 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { PROMPT_CACHE_CONTROL } from "./prompt-prefix"; +import { + BEDROCK_MODEL_IDS, + bedrockProviderSettings, + bedrockRegion, + isLongLivedCacheBreakpoint, + isStepCacheBreakpoint, + resolveDashboardAgentModel, + STEP_CACHE_CONTROL, + withCacheBreakpoint, + withoutCacheBreakpoint, +} from "./model-provider"; + +function useBedrock() { + process.env.DASHBOARD_AGENT_MODEL_PROVIDER = "bedrock"; +} + +const AWS_ENV_VARS = [ + "DASHBOARD_AGENT_MODEL_PROVIDER", + "DASHBOARD_AGENT_AWS_ACCESS_KEY_ID", + "DASHBOARD_AGENT_AWS_SECRET_ACCESS_KEY", + "DASHBOARD_AGENT_AWS_REGION", + "AWS_REGION", + "AWS_DEFAULT_REGION", +] as const; + +let priorEnv: Record; + +beforeEach(() => { + priorEnv = Object.fromEntries(AWS_ENV_VARS.map((key) => [key, process.env[key]])); + for (const key of AWS_ENV_VARS) delete process.env[key]; +}); + +afterEach(() => { + for (const key of AWS_ENV_VARS) { + if (priorEnv[key] === undefined) delete process.env[key]; + else process.env[key] = priorEnv[key]; + } +}); + +describe("resolveDashboardAgentModel", () => { + it("resolves a canonical prompt string against Anthropic by default", () => { + expect(resolveDashboardAgentModel("anthropic:claude-sonnet-4-6").modelId).toBe( + "claude-sonnet-4-6" + ); + }); + + it("maps the same canonical string to a Bedrock inference profile", () => { + useBedrock(); + expect(resolveDashboardAgentModel("anthropic:claude-sonnet-4-6").modelId).toBe( + "us.anthropic.claude-sonnet-4-6" + ); + expect(resolveDashboardAgentModel("anthropic:claude-haiku-4-5").modelId).toBe( + "us.anthropic.claude-haiku-4-5-20251001-v1:0" + ); + }); + + it("throws rather than guessing a profile for an unmapped id", () => { + useBedrock(); + expect(() => resolveDashboardAgentModel("anthropic:claude-made-up-9-9")).toThrow( + /No Bedrock model mapping/ + ); + }); + + // Pinned to Anthropic's official Bedrock model table, not a shape regex — there is + // no shared suffix convention across models, so a well-formed id can still be wrong. + it("maps every model to its exact documented Bedrock id", () => { + expect(BEDROCK_MODEL_IDS).toEqual({ + "claude-sonnet-4-6": "us.anthropic.claude-sonnet-4-6", + "claude-haiku-4-5": "us.anthropic.claude-haiku-4-5-20251001-v1:0", + }); + }); +}); + +describe("cache breakpoints", () => { + it("keeps the Anthropic cacheControl ttls intact, tagged with the discriminator", () => { + expect(withCacheBreakpoint({ openai: { store: false } }, "prefix")).toEqual({ + __cacheBreakpoint: { kind: "prefix" }, + openai: { store: false }, + anthropic: { cacheControl: PROMPT_CACHE_CONTROL }, + }); + expect(withCacheBreakpoint(undefined, "step")).toEqual({ + __cacheBreakpoint: { kind: "step" }, + anthropic: { cacheControl: STEP_CACHE_CONTROL }, + }); + }); + + it("emits a plain Bedrock cachePoint with no ttl for either marker", () => { + useBedrock(); + for (const breakpoint of ["prefix", "step"] as const) { + const options = withCacheBreakpoint(undefined, breakpoint); + // The only thing the SDK serialises to AWS is bedrock.cachePoint — it must be plain. + expect(options.bedrock.cachePoint).toEqual({ type: "default" }); + expect(options.bedrock.cachePoint).not.toHaveProperty("ttl"); + expect(options.__cacheBreakpoint).toEqual({ kind: breakpoint }); + } + }); + + it("classifies and strips the active provider's breakpoint via the discriminator", () => { + const anthropicStep = withCacheBreakpoint({ anthropic: { keep: true } }, "step"); + expect(isStepCacheBreakpoint(anthropicStep)).toBe(true); + expect(isLongLivedCacheBreakpoint(withCacheBreakpoint(undefined, "prefix"))).toBe(true); + // The strip removes both the provider field and the top-level discriminator. + expect(withoutCacheBreakpoint(anthropicStep)).toEqual({ anthropic: { keep: true } }); + + useBedrock(); + const bedrockStep = withCacheBreakpoint(undefined, "step"); + const bedrockPrefix = withCacheBreakpoint(undefined, "prefix"); + // The two Bedrock markers are byte-identical on the wire — only the tag tells them apart. + expect(bedrockStep.bedrock).toEqual(bedrockPrefix.bedrock); + expect(isStepCacheBreakpoint(bedrockStep)).toBe(true); + expect(isLongLivedCacheBreakpoint(bedrockStep)).toBe(false); + expect(isLongLivedCacheBreakpoint(bedrockPrefix)).toBe(true); + expect(withoutCacheBreakpoint(bedrockStep)).toEqual({}); + }); + + // Conversations persisted before the __cacheBreakpoint discriminator existed carry + // a bare anthropic.cacheControl. Detection must fall back to classifying its ttl. + it("classifies a legacy Anthropic cacheControl with no discriminator by its ttl", () => { + const legacyPrefix = { anthropic: { cacheControl: PROMPT_CACHE_CONTROL } }; + const legacyStepWithTtl = { anthropic: { cacheControl: STEP_CACHE_CONTROL } }; + const legacyStepNoTtl = { anthropic: { cacheControl: { type: "ephemeral" } } }; + + expect(isLongLivedCacheBreakpoint(legacyPrefix)).toBe(true); + expect(isStepCacheBreakpoint(legacyPrefix)).toBe(false); + expect(isStepCacheBreakpoint(legacyStepWithTtl)).toBe(true); + expect(isLongLivedCacheBreakpoint(legacyStepWithTtl)).toBe(false); + expect(isStepCacheBreakpoint(legacyStepNoTtl)).toBe(true); + }); + + it("strips a legacy Anthropic cacheControl even while Bedrock is active", () => { + useBedrock(); + const legacyStep = { anthropic: { cacheControl: STEP_CACHE_CONTROL, keep: true } }; + + expect(withoutCacheBreakpoint(legacyStep)).toEqual({ anthropic: { keep: true } }); + }); +}); + +describe("Bedrock region and credential resolution", () => { + it("prefers DASHBOARD_AGENT_AWS_REGION over the global AWS region vars", () => { + process.env.AWS_REGION = "us-east-1"; + process.env.AWS_DEFAULT_REGION = "us-west-2"; + process.env.DASHBOARD_AGENT_AWS_REGION = "eu-west-1"; + expect(bedrockRegion()).toBe("eu-west-1"); + }); + + it("falls back to AWS_REGION, then AWS_DEFAULT_REGION", () => { + process.env.AWS_DEFAULT_REGION = "us-west-2"; + expect(bedrockRegion()).toBe("us-west-2"); + + process.env.AWS_REGION = "us-east-1"; + expect(bedrockRegion()).toBe("us-east-1"); + }); + + it("treats an empty region as unset at every tier", () => { + process.env.DASHBOARD_AGENT_AWS_REGION = ""; + process.env.AWS_REGION = ""; + process.env.AWS_DEFAULT_REGION = ""; + expect(bedrockRegion()).toBeUndefined(); + }); + + it("passes explicit credentials when the dedicated pair is set", () => { + process.env.DASHBOARD_AGENT_AWS_ACCESS_KEY_ID = "AKIA_DASHBOARD_AGENT"; + process.env.DASHBOARD_AGENT_AWS_SECRET_ACCESS_KEY = "secret"; + process.env.DASHBOARD_AGENT_AWS_REGION = "eu-west-1"; + + expect(bedrockProviderSettings()).toEqual({ + region: "eu-west-1", + accessKeyId: "AKIA_DASHBOARD_AGENT", + secretAccessKey: "secret", + }); + }); + + it("keeps the default credential chain when the dedicated pair is unset", () => { + process.env.AWS_REGION = "us-east-1"; + expect(bedrockProviderSettings()).toEqual({ region: "us-east-1" }); + }); + + it("keeps the default chain when only one half of the dedicated pair is set", () => { + process.env.DASHBOARD_AGENT_AWS_ACCESS_KEY_ID = "AKIA_DASHBOARD_AGENT"; + expect(bedrockProviderSettings()).toEqual({ region: undefined }); + }); +}); diff --git a/internal-packages/dashboard-agent/src/model-provider.ts b/internal-packages/dashboard-agent/src/model-provider.ts new file mode 100644 index 00000000000..4d885906dff --- /dev/null +++ b/internal-packages/dashboard-agent/src/model-provider.ts @@ -0,0 +1,181 @@ +import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"; +import { anthropic } from "@ai-sdk/anthropic"; +import { createProviderRegistry } from "ai"; +import { PROMPT_CACHE_CONTROL } from "./prompt-prefix"; + +/** + * Which provider the agent's model calls go through, and the two things that + * differ between them: the model id, and the shape of the prompt-cache options. + * + * Managed prompts stay canonical `"anthropic:"` strings whichever + * provider is active, so a stored or dashboard-overridden prompt keeps meaning + * the same model. + * + * Kept free of the SDK runtime so the webapp's head-start path can import it. + */ + +export type DashboardAgentProvider = "anthropic" | "bedrock"; + +/** Global switch, read per call so it can be set per environment. */ +export function dashboardAgentProvider(): DashboardAgentProvider { + return process.env.DASHBOARD_AGENT_MODEL_PROVIDER === "bedrock" ? "bedrock" : "anthropic"; +} + +// Region passed explicitly since the SDK reads only AWS_REGION. `||` treats an empty +// region as unset. DASHBOARD_AGENT_AWS_REGION takes priority over the global vars. +export function bedrockRegion(): string | undefined { + return ( + process.env.DASHBOARD_AGENT_AWS_REGION || + process.env.AWS_REGION || + process.env.AWS_DEFAULT_REGION || + undefined + ); +} + +// Dedicated, non-global credentials only — the default chain (and the global +// AWS_ACCESS_KEY_ID/etc, if ever set) stays untouched for the ECR/STS deploy clients. +function bedrockCredentials(): { accessKeyId: string; secretAccessKey: string } | undefined { + const accessKeyId = process.env.DASHBOARD_AGENT_AWS_ACCESS_KEY_ID; + const secretAccessKey = process.env.DASHBOARD_AGENT_AWS_SECRET_ACCESS_KEY; + return accessKeyId && secretAccessKey ? { accessKeyId, secretAccessKey } : undefined; +} + +export function bedrockProviderSettings(): { + region?: string; + accessKeyId?: string; + secretAccessKey?: string; +} { + return { region: bedrockRegion(), ...bedrockCredentials() }; +} + +const bedrock = createAmazonBedrock(bedrockProviderSettings()); + +export const registry = createProviderRegistry({ anthropic, bedrock }); + +/** + * Canonical model id -> Bedrock us cross-region inference profile, verbatim from + * Anthropic's official Bedrock model table. No shared suffix convention across + * models — copy each id exactly rather than deriving it. + */ +export const BEDROCK_MODEL_IDS: Record = { + "claude-sonnet-4-6": "us.anthropic.claude-sonnet-4-6", + "claude-haiku-4-5": "us.anthropic.claude-haiku-4-5-20251001-v1:0", +}; + +/** Resolve a canonical `"anthropic:"` string against the active provider. */ +export function resolveDashboardAgentModel(model: string) { + const id = model.startsWith("anthropic:") ? model.slice("anthropic:".length) : model; + if (dashboardAgentProvider() === "anthropic") { + return registry.languageModel(`anthropic:${id}` as `anthropic:${string}`); + } + const bedrockId = BEDROCK_MODEL_IDS[id]; + if (!bedrockId) { + // No Bedrock profile can be guessed from the canonical id — a made-up one is a + // guaranteed 404, so fail loudly instead. + throw new Error(`No Bedrock model mapping for "${id}"`); + } + return registry.languageModel(`bedrock:${bedrockId}` as `bedrock:${string}`); +} + +/** + * The two breakpoints a turn sets: the prefix one that spans the turn, and the + * rolling per-step one. + */ +export type CacheBreakpoint = "prefix" | "step"; + +export const STEP_CACHE_CONTROL = { type: "ephemeral", ttl: "5m" } as const; + +type ProviderOptions = Record | undefined; + +// Breakpoint discriminator under a top-level key no provider serialises. Value is an +// object because the AI SDK validates providerOptions as records, rejecting a bare string. +const CACHE_BREAKPOINT_KEY = "__cacheBreakpoint"; + +function breakpointKind(providerOptions: ProviderOptions): CacheBreakpoint | undefined { + const discriminated = providerOptions?.[CACHE_BREAKPOINT_KEY]?.kind; + if (discriminated) return discriminated; + // Conversations persisted before the discriminator existed carry a bare Anthropic + // cacheControl. Classify it by ttl: "1h" is the turn-wide prefix, anything else the step. + const legacyCacheControl = providerOptions?.anthropic?.cacheControl; + if (!legacyCacheControl) return undefined; + return legacyCacheControl.ttl === "1h" ? "prefix" : "step"; +} + +function cacheOptions(breakpoint: CacheBreakpoint): Record { + if (dashboardAgentProvider() === "anthropic") { + return { + anthropic: { + cacheControl: breakpoint === "prefix" ? PROMPT_CACHE_CONTROL : STEP_CACHE_CONTROL, + }, + }; + } + // Plain, documented cachePoint for both markers — nothing undocumented reaches AWS. + return { bedrock: { cachePoint: { type: "default" } } }; +} + +/** Merge the active provider's breakpoint into a message's provider options. */ +export function withCacheBreakpoint( + providerOptions: ProviderOptions, + breakpoint: CacheBreakpoint +): Record { + const [key, options] = Object.entries(cacheOptions(breakpoint))[0]!; + return { + ...providerOptions, + [CACHE_BREAKPOINT_KEY]: { kind: breakpoint }, + [key]: { ...providerOptions?.[key], ...options }, + }; +} + +/** + * Whether these options carry the rolling step breakpoint — the one the step-strip + * pass rolls off. + */ +export function isStepCacheBreakpoint(providerOptions: ProviderOptions): boolean { + return breakpointKind(providerOptions) === "step"; +} + +/** Whether these options carry a breakpoint that outlives a step (the turn-wide prefix). */ +export function isLongLivedCacheBreakpoint(providerOptions: ProviderOptions): boolean { + return breakpointKind(providerOptions) === "prefix"; +} + +/** + * The cache token counts the active provider reports on a call's metadata. + * Bedrock puts only the write there; its read count reaches the call's usage. + */ +export function cacheUsageFromProviderMetadata(providerMetadata: unknown): { + write?: number; + read?: number; +} { + const metadata = providerMetadata as Record | undefined; + const count = (value: unknown) => (typeof value === "number" ? value : undefined); + if (dashboardAgentProvider() === "anthropic") { + return { + write: count(metadata?.anthropic?.cacheCreationInputTokens), + read: count(metadata?.anthropic?.cacheReadInputTokens), + }; + } + return { write: count(metadata?.bedrock?.usage?.cacheWriteInputTokens) }; +} + +/** The same options with the active provider's breakpoint and its discriminator removed. */ +export function withoutCacheBreakpoint(providerOptions: ProviderOptions): Record { + const hasDiscriminator = providerOptions?.[CACHE_BREAKPOINT_KEY] !== undefined; + // A legacy message keeps its native anthropic.cacheControl shape no matter which + // provider is active now, so strip that key rather than the current provider's. + const isLegacy = !hasDiscriminator && providerOptions?.anthropic?.cacheControl !== undefined; + const key = isLegacy + ? "anthropic" + : dashboardAgentProvider() === "anthropic" + ? "anthropic" + : "bedrock"; + const field = key === "anthropic" ? "cacheControl" : "cachePoint"; + const { + [key]: provider, + [CACHE_BREAKPOINT_KEY]: _tag, + ...rest + } = (providerOptions ?? {}) as Record; + const { [field]: _dropped, ...providerRest } = (provider ?? {}) as Record; + // An empty provider entry is not the same as no options for it, so drop the key. + return Object.keys(providerRest).length > 0 ? { ...rest, [key]: providerRest } : rest; +} diff --git a/internal-packages/dashboard-agent/src/step-cache.test.ts b/internal-packages/dashboard-agent/src/step-cache.test.ts index 662afc012b0..b43c07401c4 100644 --- a/internal-packages/dashboard-agent/src/step-cache.test.ts +++ b/internal-packages/dashboard-agent/src/step-cache.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { markStepCacheBreakpoint, MIN_STEP_CACHE_CHARS, @@ -7,6 +7,7 @@ import { withStepCacheBreakpoint, } from "./step-cache"; import { PROMPT_CACHE_CONTROL } from "./prompt-prefix"; +import { withCacheBreakpoint } from "./model-provider"; type Message = { role: string; @@ -22,7 +23,10 @@ function turnHistory(): Message { return { role: "user", content: "why did run_1 fail?", - providerOptions: { anthropic: { cacheControl: PROMPT_CACHE_CONTROL } }, + providerOptions: { + __cacheBreakpoint: { kind: "prefix" }, + anthropic: { cacheControl: PROMPT_CACHE_CONTROL }, + }, }; } @@ -31,6 +35,7 @@ function stepBreakpointWith(otherAnthropicOptions: Record): Mes role: "tool", content: "ok", providerOptions: { + __cacheBreakpoint: { kind: "step" }, anthropic: { cacheControl: STEP_CACHE_CONTROL, ...otherAnthropicOptions }, openai: { store: false }, }, @@ -107,6 +112,7 @@ describe("the step cache breakpoint", () => { ]); expect(marked.at(-1)!.providerOptions).toEqual({ + __cacheBreakpoint: { kind: "step" }, anthropic: { anotherOption: "keep", cacheControl: STEP_CACHE_CONTROL }, openai: { store: false }, }); @@ -125,6 +131,83 @@ describe("the step cache breakpoint", () => { const empty: Message[] = []; expect(markStepCacheBreakpoint(empty)).toBe(empty); }); + + // A conversation resumed after this branch shipped can still carry a step + // breakpoint in the pre-discriminator shape. It must be stripped like any other. + it("strips a legacy step breakpoint on resume", () => { + const legacyStep: Message = { + role: "tool", + content: "ok", + providerOptions: { anthropic: { cacheControl: STEP_CACHE_CONTROL, keep: true } }, + }; + const marked = markStepCacheBreakpoint([turnHistory(), legacyStep, toolResult(20)]); + + expect(ttlOf(marked[1])).toBeUndefined(); + expect(marked[1]!.providerOptions).toEqual({ anthropic: { keep: true } }); + expect(ttlOf(marked[0])).toBe("1h"); + }); +}); + +describe("the step cache breakpoint on Bedrock", () => { + let priorProvider: string | undefined; + beforeEach(() => { + priorProvider = process.env.DASHBOARD_AGENT_MODEL_PROVIDER; + process.env.DASHBOARD_AGENT_MODEL_PROVIDER = "bedrock"; + }); + afterEach(() => { + if (priorProvider === undefined) delete process.env.DASHBOARD_AGENT_MODEL_PROVIDER; + else process.env.DASHBOARD_AGENT_MODEL_PROVIDER = priorProvider; + }); + + function bedrockCachePoint(message: Message | undefined): { ttl?: unknown } | undefined { + return (message?.providerOptions?.bedrock as { cachePoint?: { ttl?: unknown } } | undefined) + ?.cachePoint; + } + + function breakpointTag(message: Message | undefined): unknown { + return (message?.providerOptions?.__cacheBreakpoint as { kind?: unknown } | undefined)?.kind; + } + + function prefixMarker(): Message { + return { + role: "user", + content: "why did run_1 fail?", + providerOptions: withCacheBreakpoint(undefined, "prefix"), + }; + } + + // Nothing undocumented reaches AWS: the wire cachePoint is a plain `{type:"default"}` + // for both markers. The prefix/step distinction lives only in the `__cacheBreakpoint` tag. + it("emits a plain cachePoint with no ttl for either marker", () => { + expect(bedrockCachePoint(prefixMarker())).toEqual({ type: "default" }); + const step: Message = { + role: "tool", + content: "ok", + providerOptions: withCacheBreakpoint(undefined, "step"), + }; + expect(bedrockCachePoint(step)).toEqual({ type: "default" }); + expect(bedrockCachePoint(step)).not.toHaveProperty("ttl"); + expect(breakpointTag(prefixMarker())).toBe("prefix"); + expect(breakpointTag(step)).toBe("step"); + }); + + // The turn-wide prefix marker sits on the last message; a short conversation never + // earns a step marker, so stripping the prefix would leave the history uncached. + it("keeps the turn-wide prefix cachePoint on a short conversation", () => { + const marked = markStepCacheBreakpoint([prefixMarker()]); + + expect(bedrockCachePoint(marked.at(-1))).toEqual({ type: "default" }); + expect(breakpointTag(marked.at(-1))).toBe("prefix"); + }); + + it("rolls the per-step cachePoint onto the tail once it is worth caching", () => { + const marked = markStepCacheBreakpoint([prefixMarker(), toolResult(MIN_STEP_CACHE_CHARS)]); + + expect(bedrockCachePoint(marked[0])).toEqual({ type: "default" }); + expect(breakpointTag(marked[0])).toBe("prefix"); + expect(bedrockCachePoint(marked.at(-1))).toEqual({ type: "default" }); + expect(breakpointTag(marked.at(-1))).toBe("step"); + }); }); describe("wrapping the SDK's prepareStep", () => { @@ -162,6 +245,7 @@ describe("wrapping the SDK's prepareStep", () => { const prepared = await withStepCacheBreakpoint(inner as never)({ messages: [] } as never); expect((prepared!.messages!.at(-1) as Message).providerOptions).toEqual({ + __cacheBreakpoint: { kind: "step" }, anthropic: { anotherOption: "keep", cacheControl: STEP_CACHE_CONTROL }, }); }); @@ -189,6 +273,27 @@ describe("per-step cache telemetry", () => { }); }); + it("reports Bedrock's write from its metadata and its read from the call's usage", () => { + const prior = process.env.DASHBOARD_AGENT_MODEL_PROVIDER; + process.env.DASHBOARD_AGENT_MODEL_PROVIDER = "bedrock"; + try { + expect( + stepCacheAttributes( + 2, + { bedrock: { usage: { cacheWriteInputTokens: 8_000 } } }, + { inputTokenDetails: { cacheReadTokens: 12_000 } } + ) + ).toEqual({ + "dashboard_agent.step": 2, + "gen_ai.usage.cache_creation_input_tokens": 8_000, + "gen_ai.usage.cache_read_input_tokens": 12_000, + }); + } finally { + if (prior === undefined) delete process.env.DASHBOARD_AGENT_MODEL_PROVIDER; + else process.env.DASHBOARD_AGENT_MODEL_PROVIDER = prior; + } + }); + it("reports null rather than zero when the provider said nothing", () => { expect(stepCacheAttributes(0, undefined)).toEqual({ "dashboard_agent.step": 0, diff --git a/internal-packages/dashboard-agent/src/step-cache.ts b/internal-packages/dashboard-agent/src/step-cache.ts index 56465a0ee9a..b6306072a52 100644 --- a/internal-packages/dashboard-agent/src/step-cache.ts +++ b/internal-packages/dashboard-agent/src/step-cache.ts @@ -1,5 +1,12 @@ import { logger } from "@trigger.dev/sdk"; import type { ModelMessage, ToolSet } from "ai"; +import { + cacheUsageFromProviderMetadata, + isLongLivedCacheBreakpoint, + isStepCacheBreakpoint, + withCacheBreakpoint, + withoutCacheBreakpoint, +} from "./model-provider"; import { describePromptPrefix, promptCacheAttributes, @@ -15,36 +22,16 @@ import { * its accumulated tool outputs uncached on every step. */ -export const STEP_CACHE_CONTROL = { type: "ephemeral", ttl: "5m" } as const; +export { STEP_CACHE_CONTROL } from "./model-provider"; // Anthropic silently refuses to cache a prefix shorter than roughly 1024 tokens. export const MIN_STEP_CACHE_CHARS = 4_096; type MaybeCached = { providerOptions?: Record }; -function cacheControlTtl(message: MaybeCached): string | undefined { - const anthropic = message.providerOptions?.anthropic as - | { cacheControl?: { ttl?: unknown } } - | undefined; - const ttl = anthropic?.cacheControl?.ttl; - return typeof ttl === "string" ? ttl : undefined; -} - -function anthropicOptions(message: MaybeCached): Record { - const anthropic = message.providerOptions?.anthropic; - return typeof anthropic === "object" && anthropic !== null - ? (anthropic as Record) - : {}; -} - function withoutStepBreakpoint(message: T): T { - if (cacheControlTtl(message) !== STEP_CACHE_CONTROL.ttl) return message; - const { anthropic, ...rest } = message.providerOptions as Record; - const { cacheControl: _dropped, ...anthropicRest } = anthropic as Record; - // An empty `anthropic` is not the same as no Anthropic options, so drop the key. - const providerOptions = - Object.keys(anthropicRest).length > 0 ? { ...rest, anthropic: anthropicRest } : rest; - return { ...message, providerOptions }; + if (!isStepCacheBreakpoint(message.providerOptions)) return message; + return { ...message, providerOptions: withoutCacheBreakpoint(message.providerOptions) }; } // Only ever one step breakpoint at a time: Anthropic allows four in total, and the @@ -54,8 +41,7 @@ export function markStepCacheBreakpoint(messages: T[]): T let lastLongLived = -1; messages.forEach((message, index) => { - const ttl = cacheControlTtl(message); - if (ttl !== undefined && ttl !== STEP_CACHE_CONTROL.ttl) lastLongLived = index; + if (isLongLivedCacheBreakpoint(message.providerOptions)) lastLongLived = index; }); const tail = messages.slice(lastLongLived + 1); if ((JSON.stringify(tail)?.length ?? 0) < MIN_STEP_CACHE_CHARS) { @@ -66,13 +52,7 @@ export function markStepCacheBreakpoint(messages: T[]): T const last = stripped[stripped.length - 1]!; return [ ...stripped.slice(0, -1), - { - ...last, - providerOptions: { - ...last.providerOptions, - anthropic: { ...anthropicOptions(last), cacheControl: STEP_CACHE_CONTROL }, - }, - }, + { ...last, providerOptions: withCacheBreakpoint(last.providerOptions, "step") }, ]; } @@ -97,16 +77,16 @@ export function stepCachePrepareStep(options: unknown): PrepareStepFn { export function stepCacheAttributes( step: number | undefined, - providerMetadata: unknown + providerMetadata: unknown, + usage?: PromptCacheUsage ): Record { - const anthropic = (providerMetadata as { anthropic?: Record } | undefined) - ?.anthropic; - const write = anthropic?.cacheCreationInputTokens; - const read = anthropic?.cacheReadInputTokens; + const { write, read } = cacheUsageFromProviderMetadata(providerMetadata); return { "dashboard_agent.step": step ?? null, - "gen_ai.usage.cache_creation_input_tokens": typeof write === "number" ? write : null, - "gen_ai.usage.cache_read_input_tokens": typeof read === "number" ? read : null, + "gen_ai.usage.cache_creation_input_tokens": + write ?? usage?.inputTokenDetails?.cacheWriteTokens ?? null, + "gen_ai.usage.cache_read_input_tokens": + read ?? usage?.inputTokenDetails?.cacheReadTokens ?? null, }; } @@ -131,7 +111,7 @@ export function recordPromptCacheUsage(args: { usage: args.usage, prefix: describePromptPrefix({ system: args.system, tools: args.tools }), }), - ...stepCacheAttributes(args.step, args.providerMetadata), + ...stepCacheAttributes(args.step, args.providerMetadata, args.usage), }); } catch (error) { // Measurement must never fail a turn. diff --git a/internal-packages/dashboard-agent/src/watch-actions.ts b/internal-packages/dashboard-agent/src/watch-actions.ts index ddb46e0f4d1..af356f0f4bc 100644 --- a/internal-packages/dashboard-agent/src/watch-actions.ts +++ b/internal-packages/dashboard-agent/src/watch-actions.ts @@ -28,7 +28,7 @@ import { getStore, getSystemPrompt, modeFor, - registry, + resolveDashboardAgentModel, latestCards, sanitizeReplayedToolInputs, clearOpenInvestigations, @@ -428,7 +428,7 @@ async function narrateWithPlan(input: { ? streamText({ model: locals.get(dashboardAgentModelKey) ?? - registry.languageModel("anthropic:claude-haiku-4-5"), + resolveDashboardAgentModel("anthropic:claude-haiku-4-5"), system: HAIKU_WAKE_BRIEF, // Bounded on purpose: the wake alone, no conversation and no tools. messages: [ @@ -442,9 +442,7 @@ async function narrateWithPlan(input: { : streamText({ model: locals.get(dashboardAgentModelKey) ?? - registry.languageModel( - (resolved.model ?? "anthropic:claude-sonnet-4-6") as `anthropic:${string}` - ), + resolveDashboardAgentModel(resolved.model ?? "anthropic:claude-sonnet-4-6"), system: resolved.text, // No tools: a wake reports what the check already established, and carries no // delegated token to read with. The breakpoint goes on the last message of the @@ -782,9 +780,7 @@ async function conductWatchInvestigation(args: { const result = streamText({ model: locals.get(dashboardAgentModelKey) ?? - registry.languageModel( - (resolved.model ?? "anthropic:claude-sonnet-4-6") as `anthropic:${string}` - ), + resolveDashboardAgentModel(resolved.model ?? "anthropic:claude-sonnet-4-6"), system: resolved.text, tools, // Ten steps of accumulating tool output is exactly what the rolling breakpoint diff --git a/internal-packages/rbac/package.json b/internal-packages/rbac/package.json index 53374670f7e..e68e80e5c55 100644 --- a/internal-packages/rbac/package.json +++ b/internal-packages/rbac/package.json @@ -9,13 +9,14 @@ "@trigger.dev/plugins": "workspace:*" }, "devDependencies": { + "@internal/testcontainers": "workspace:*", "@trigger.dev/database": "workspace:*", "@types/node": "^24.13.3", "rimraf": "6.0.1" }, "scripts": { "clean": "rimraf dist", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit -p tsconfig.src.json && tsc --noEmit -p tsconfig.test.json", "build": "pnpm run clean && tsc -p tsconfig.build.json", "dev": "tsc --noEmit false --outDir dist --declaration --watch", "test": "vitest run", diff --git a/internal-packages/rbac/src/fallback.ts b/internal-packages/rbac/src/fallback.ts index 2d9bb9cea91..5a7cdddd83a 100644 --- a/internal-packages/rbac/src/fallback.ts +++ b/internal-packages/rbac/src/fallback.ts @@ -260,6 +260,29 @@ class RoleBaseAccessFallbackController implements RoleBaseAccessController { if (!claims) { return { ok: false, status: 401, error: "Invalid user-actor token" }; } + + // Same tenant floor as authenticateSession: in a scoped context a non-member's + // delegated token is denied here, not handed a usable ability (even for reads). + // Admins are exempt. An unscoped context is not a tenant claim — skip the lookup + // entirely and keep the prior behavior (no user query, no denial). + if (context.organizationId || context.projectId) { + const where = { id: claims.userId }; + const user = + (await this.replica.user.findFirst({ where, select: { id: true, admin: true } })) ?? + (await this.prisma.user.findFirst({ where, select: { id: true, admin: true } })); + if (!user) { + return { ok: false, status: 401, error: "Invalid user-actor token" }; + } + if (!user.admin) { + const denied = await this.deniedByMembership( + context.organizationId, + context.projectId, + user.id + ); + if (denied) return { ok: false, status: 403, error: "Unauthorized" }; + } + } + return { ok: true, userId: claims.userId, diff --git a/internal-packages/rbac/src/fallback.userActor.test.ts b/internal-packages/rbac/src/fallback.userActor.test.ts new file mode 100644 index 00000000000..2423f7419f8 --- /dev/null +++ b/internal-packages/rbac/src/fallback.userActor.test.ts @@ -0,0 +1,102 @@ +import type { PrismaClient } from "@trigger.dev/database"; +import { signUserActorToken } from "@trigger.dev/plugins"; +import { postgresTest } from "@internal/testcontainers"; +import { expect } from "vitest"; +import { RoleBaseAccessFallback } from "./fallback.js"; + +const SECRET = "test-user-actor-secret"; + +function uatRequest(token: string): Request { + return new Request("https://example.test", { + headers: { Authorization: `Bearer ${token}` }, + }); +} + +async function seedUser(prisma: PrismaClient, email: string, admin = false) { + return prisma.user.create({ + data: { email, authenticationMethod: "MAGIC_LINK", admin }, + }); +} + +async function uat(userId: string) { + return signUserActorToken(SECRET, { userId, client: "test" }); +} + +postgresTest( + "authenticateUserActor: scoped membership floor", + async ({ prisma }) => { + const p = prisma as PrismaClient; + const org = await p.organization.create({ + data: { slug: `org-${Date.now()}`, title: "Org" }, + }); + const project = await p.project.create({ + data: { + slug: `proj-${Date.now()}`, + name: "Project", + externalRef: `ref-${Date.now()}`, + organizationId: org.id, + }, + }); + const member = await seedUser(p, "member@example.test"); + const stranger = await seedUser(p, "stranger@example.test"); + const admin = await seedUser(p, "admin@example.test", true); + await p.orgMember.create({ data: { organizationId: org.id, userId: member.id } }); + + const controller = new RoleBaseAccessFallback(p, { userActorSecret: SECRET }).create(); + + // Member with a capless token keeps the read:all default. + const memberResult = await controller.authenticateUserActor(uatRequest(await uat(member.id)), { + organizationId: org.id, + }); + expect(memberResult.ok).toBe(true); + if (memberResult.ok) { + expect(memberResult.ability.can("read", { type: "runs", id: "run_x" })).toBe(true); + } + + // Non-member is denied at the ability layer, not handed a usable ability. + const strangerResult = await controller.authenticateUserActor( + uatRequest(await uat(stranger.id)), + { organizationId: org.id } + ); + expect(strangerResult.ok).toBe(false); + if (!strangerResult.ok) expect(strangerResult.status).toBe(403); + + // A token for a user that no longer exists fails closed. + const ghostResult = await controller.authenticateUserActor(uatRequest(await uat("usr_ghost")), { + organizationId: org.id, + }); + expect(ghostResult.ok).toBe(false); + if (!ghostResult.ok) expect(ghostResult.status).toBe(401); + + // A platform admin is exempt from the membership floor. + const adminResult = await controller.authenticateUserActor(uatRequest(await uat(admin.id)), { + organizationId: org.id, + }); + expect(adminResult.ok).toBe(true); + + // A project-only scope resolves through the project's org: non-member denied. + const projectResult = await controller.authenticateUserActor( + uatRequest(await uat(stranger.id)), + { projectId: project.id } + ); + expect(projectResult.ok).toBe(false); + if (!projectResult.ok) expect(projectResult.status).toBe(403); + }, + 120_000 +); + +postgresTest( + "authenticateUserActor: unscoped context skips the floor and never queries the user", + async ({ prisma }) => { + const p = prisma as PrismaClient; + const controller = new RoleBaseAccessFallback(p, { userActorSecret: SECRET }).create(); + + // A user that doesn't exist: if the unscoped path ran the lookup this would 401. + const result = await controller.authenticateUserActor(uatRequest(await uat("usr_ghost")), {}); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.ability.can("read", { type: "runs", id: "run_x" })).toBe(true); + } + }, + 120_000 +); diff --git a/internal-packages/rbac/tsconfig.json b/internal-packages/rbac/tsconfig.json index 9ab7d813559..2b8d67a9025 100644 --- a/internal-packages/rbac/tsconfig.json +++ b/internal-packages/rbac/tsconfig.json @@ -13,5 +13,7 @@ "strict": true, "customConditions": ["@triggerdotdev/source"] }, - "exclude": ["node_modules", "dist"] + // Excluded from this IDE-default project: needs ES2022 lib to type-check + // (see tsconfig.test.json). typecheck script still checks it. + "exclude": ["node_modules", "dist", "src/fallback.userActor.test.ts"] } diff --git a/internal-packages/rbac/tsconfig.src.json b/internal-packages/rbac/tsconfig.src.json new file mode 100644 index 00000000000..fc3f31fd51c --- /dev/null +++ b/internal-packages/rbac/tsconfig.src.json @@ -0,0 +1,19 @@ +{ + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts"], + "compilerOptions": { + "target": "ES2019", + "lib": ["ES2019", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "preserveWatchOutput": true, + "skipLibCheck": true, + "noEmit": true, + "strict": true, + "types": ["node"], + "customConditions": ["@triggerdotdev/source"] + } +} diff --git a/internal-packages/rbac/tsconfig.test.json b/internal-packages/rbac/tsconfig.test.json new file mode 100644 index 00000000000..03817c4b125 --- /dev/null +++ b/internal-packages/rbac/tsconfig.test.json @@ -0,0 +1,18 @@ +{ + "include": ["src/**/*.test.ts"], + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "preserveWatchOutput": true, + "skipLibCheck": true, + "noEmit": true, + "strict": true, + "types": ["vitest/globals", "node"], + "customConditions": ["@triggerdotdev/source"] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a65d42dbb5..d7ea4d79503 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -958,6 +958,9 @@ importers: internal-packages/dashboard-agent: dependencies: + '@ai-sdk/amazon-bedrock': + specifier: 4.0.117 + version: 4.0.117(zod@3.25.76) '@ai-sdk/anthropic': specifier: ^3.0.0 version: 3.0.84(zod@3.25.76) @@ -1163,6 +1166,9 @@ importers: specifier: workspace:* version: link:../../packages/plugins devDependencies: + '@internal/testcontainers': + specifier: workspace:* + version: link:../testcontainers '@trigger.dev/database': specifier: workspace:* version: link:../database @@ -2113,6 +2119,12 @@ importers: packages: + '@ai-sdk/amazon-bedrock@4.0.117': + resolution: {integrity: sha512-MebXAEsdvNdzKZCbVxFK0668KhYrs6W3mMH5fWT5Yc0TMSSTbwPI9fIw0sOzxLWd9TE9cJz73vVPjQjtG3vj/Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/anthropic@3.0.84': resolution: {integrity: sha512-BIDaHmCHs6Sr5VUsEkTbbVlAN4GWjg97X9x/IfXyviLtzsXvffui9XIcZugkAi1Ri6FnvI5T5qDGh5YLnSuzRg==} engines: {node: '>=18'} @@ -2143,6 +2155,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/openai@3.0.71': + resolution: {integrity: sha512-j6eBAa5oHFZ4U5CxpIV3T4zXNM/BviodNCZCL1qHkA4aqkwK9iQ18TWYz2DZcXpw4BO5pikKzqpXORxb1EnZGA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/otel@1.0.0-beta.6': resolution: {integrity: sha512-K5VikyO3EKQkNk77ew9oMjM8FInKF+WWar599LmP8rQ0x0iB+P/DVS+h6zQvmecxMNPtQOOyt0uDQFx/AA0DGw==} engines: {node: '>=18'} @@ -8546,6 +8564,9 @@ packages: aws4fetch@1.0.18: resolution: {integrity: sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ==} + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + axios@1.19.0: resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} @@ -15076,6 +15097,17 @@ packages: snapshots: + '@ai-sdk/amazon-bedrock@4.0.117(zod@3.25.76)': + dependencies: + '@ai-sdk/anthropic': 3.0.84(zod@3.25.76) + '@ai-sdk/openai': 3.0.71(zod@3.25.76) + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.29(zod@3.25.76) + '@smithy/eventstream-codec': 4.2.5 + '@smithy/util-utf8': 4.2.0 + aws4fetch: 1.0.20 + zod: 3.25.76 + '@ai-sdk/anthropic@3.0.84(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.10 @@ -15109,6 +15141,12 @@ snapshots: '@ai-sdk/provider-utils': 4.0.29(zod@3.25.76) zod: 3.25.76 + '@ai-sdk/openai@3.0.71(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.29(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/otel@1.0.0-beta.6(zod@3.25.76)': dependencies: '@ai-sdk/provider': 4.0.0-beta.5 @@ -21471,7 +21509,7 @@ snapshots: '@smithy/node-http-handler': 4.4.5 '@smithy/types': 4.9.0 '@smithy/util-base64': 4.3.0 - '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-buffer-from': 4.2.0 '@smithy/util-hex-encoding': 4.2.0 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 @@ -22750,6 +22788,8 @@ snapshots: aws4fetch@1.0.18: {} + aws4fetch@1.0.20: {} + axios@1.19.0: dependencies: follow-redirects: 1.16.0 @@ -27006,7 +27046,7 @@ snapshots: node-abi@3.89.0: dependencies: - semver: 7.8.5 + semver: 7.8.1 optional: true node-abort-controller@3.1.1: {} From 53ca44dd2dd0c06a60eda0cad286c03792f9ae48 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 18 Aug 2026 12:58:47 +0100 Subject: [PATCH 06/98] chore: cache and clean up Knip analysis (#4658) --- apps/webapp/package.json | 1 - internal-packages/dashboard-agent/src/agent-runtime.ts | 2 +- package.json | 2 +- pnpm-lock.yaml | 3 --- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 8e232b14795..6fff042a7e7 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -27,7 +27,6 @@ "eval:dev": "evalite watch" }, "dependencies": { - "@ai-sdk/anthropic": "^3.0.0", "@ai-sdk/openai": "^3.0.0", "@ai-sdk/react": "^3.0.0", "@ariakit/react": "^0.4.6", diff --git a/internal-packages/dashboard-agent/src/agent-runtime.ts b/internal-packages/dashboard-agent/src/agent-runtime.ts index fe1d99e3699..0058f04bb75 100644 --- a/internal-packages/dashboard-agent/src/agent-runtime.ts +++ b/internal-packages/dashboard-agent/src/agent-runtime.ts @@ -57,7 +57,7 @@ function getDb(): DashboardAgentDbClient { // Resolves the `"provider:model-id"` strings on our managed prompts to AI SDK // models, against whichever provider is switched on. -export { registry, resolveDashboardAgentModel } from "./model-provider"; +export { resolveDashboardAgentModel } from "./model-provider"; // The agent's persistence, behind an interface so tests can inject a fake via // `locals` and never need a real database. diff --git a/package.json b/package.json index 4b4b4c7d8e8..60b439de5d7 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "format:prisma": "pnpm --filter @trigger.dev/database run format:prisma && pnpm --filter @internal/run-ops-database run format:prisma", "lint": "oxlint", "lint:fix": "oxlint --fix", - "knip": "knip --include files,exports,types,dependencies,unlisted,binaries,unresolved,catalog", + "knip": "knip --cache --include files,exports,types,dependencies,unlisted,binaries,unresolved,catalog", "docker": "node scripts/docker.mjs -f docker/docker-compose.yml up -d --build --remove-orphans", "docker:stop": "node scripts/docker.mjs -f docker/docker-compose.yml stop", "docker:full": "node scripts/docker.mjs -f docker/docker-compose.yml -f docker/docker-compose.extras.yml up -d --build --remove-orphans", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7ea4d79503..d2ab76bcb2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,9 +200,6 @@ importers: apps/webapp: dependencies: - '@ai-sdk/anthropic': - specifier: ^3.0.0 - version: 3.0.84(zod@3.25.76) '@ai-sdk/openai': specifier: ^3.0.0 version: 3.0.41(zod@3.25.76) From 7e677008edbf2e96ba7e3e204e5dcdf0a29567c6 Mon Sep 17 00:00:00 2001 From: Saadi Myftija Date: Tue, 18 Aug 2026 14:16:23 +0200 Subject: [PATCH 07/98] feat(supervisor): per-org placement overrides for run pods (#4655) The supervisor now supports routing an organization's runs to specific nodes. `KUBERNETES_ORG_PLACEMENT_OVERRIDES` takes JSON keyed by the internal org ID, adding node selector entries and tolerations to that org's run pods, e.g. to route an org onto a dedicated, tainted node pool: ```json {"": {"nodeSelector": {"pool": "dedicated"}, "tolerations": "dedicated=runs:NoSchedule"}} ``` The node selector merges over the defaults (the override wins on key collision, with a warning logged). Tolerations append to the existing runner and scheduled-run sets. Overrides are validated at startup similar to `KUBERNETES_RUNNER_TOLERATIONS`. Exposed in the Helm chart as `supervisor.config.kubernetes.orgPlacementOverrides`, where tolerations can also be given as a list. --- .../supervisor-org-placement-overrides.md | 6 + apps/supervisor/src/env.ts | 29 +++- apps/supervisor/src/envUtil.test.ts | 130 +++++++++++++++++- apps/supervisor/src/envUtil.ts | 96 +++++++++++++ .../src/workloadManager/kubernetes.test.ts | 44 ++++++ .../src/workloadManager/kubernetes.ts | 36 ++++- .../src/workloadManager/kubernetesPodSpec.ts | 30 +++- docs/self-hosting/env/supervisor.mdx | 1 + hosting/k8s/helm/templates/supervisor.yaml | 4 + hosting/k8s/helm/values.yaml | 3 + 10 files changed, 370 insertions(+), 9 deletions(-) create mode 100644 .server-changes/supervisor-org-placement-overrides.md diff --git a/.server-changes/supervisor-org-placement-overrides.md b/.server-changes/supervisor-org-placement-overrides.md new file mode 100644 index 00000000000..532c4a73e2a --- /dev/null +++ b/.server-changes/supervisor-org-placement-overrides.md @@ -0,0 +1,6 @@ +--- +area: supervisor +type: feature +--- + +Operators can now route an organization's runs to specific Kubernetes node pools. diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index 6830d5b8642..c5aaf70e1fe 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -1,7 +1,13 @@ import { randomUUID } from "crypto"; import { env as stdEnv } from "std-env"; import { z } from "zod"; -import { AdditionalEnvVars, BoolEnv, NodeLabelValue, Tolerations } from "./envUtil.js"; +import { + AdditionalEnvVars, + BoolEnv, + NodeLabelValue, + OrgPlacementOverrides, + Tolerations, +} from "./envUtil.js"; export const Env = z .object({ @@ -260,6 +266,11 @@ export const Env = z KUBERNETES_RUNNER_TOLERATIONS: Tolerations.optional(), // every run pod KUBERNETES_SCHEDULED_RUN_TOLERATIONS: Tolerations.optional(), // schedule-tree runs only + // Per-org placement overrides, JSON keyed by the internal org id + // (the `org` label on run pods): + // {"": {"nodeSelector": {"": ""}, "tolerations": ""}} + KUBERNETES_ORG_PLACEMENT_OVERRIDES: OrgPlacementOverrides, + // Placement tags settings PLACEMENT_TAGS_ENABLED: BoolEnv.default(false), PLACEMENT_TAGS_PREFIX: z.string().default("node.cluster.x-k8s.io"), @@ -305,6 +316,22 @@ export const Env = z path: ["TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_RELEASE"], }); } + if (data.KUBERNETES_LARGE_MACHINE_AFFINITY_ENABLED && data.KUBERNETES_ORG_PLACEMENT_OVERRIDES) { + // Non-large presets carry a hard NotIn on the large-machine pool, so an org + // pinned to that pool could never schedule its non-large runs. + for (const [orgId, override] of Object.entries(data.KUBERNETES_ORG_PLACEMENT_OVERRIDES)) { + const pinnedPool = + override.nodeSelector?.[data.KUBERNETES_LARGE_MACHINE_AFFINITY_POOL_LABEL_KEY]; + + if (pinnedPool === data.KUBERNETES_LARGE_MACHINE_AFFINITY_POOL_LABEL_VALUE) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Org "${orgId}" pins run pods to the large-machine pool, but non-large presets are required to stay off it, so those runs would never schedule. Use a different pool or disable KUBERNETES_LARGE_MACHINE_AFFINITY_ENABLED.`, + path: ["KUBERNETES_ORG_PLACEMENT_OVERRIDES"], + }); + } + } + } if (data.COMPUTE_SNAPSHOTS_ENABLED && !data.TRIGGER_METADATA_URL) { ctx.addIssue({ code: z.ZodIssueCode.custom, diff --git a/apps/supervisor/src/envUtil.test.ts b/apps/supervisor/src/envUtil.test.ts index 378830f8ab0..9231925303f 100644 --- a/apps/supervisor/src/envUtil.test.ts +++ b/apps/supervisor/src/envUtil.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect } from "vitest"; -import { BoolEnv, AdditionalEnvVars, NodeLabelValue, Tolerations } from "./envUtil.js"; +import { + BoolEnv, + AdditionalEnvVars, + NodeLabelValue, + OrgPlacementOverrides, + Tolerations, +} from "./envUtil.js"; describe("BoolEnv", () => { it("should parse string 'true' as true", () => { @@ -203,3 +209,125 @@ describe("Tolerations", () => { expect(Tolerations.safeParse("dedicated=runs:NoSchedule:NoExecute").success).toBe(false); }); }); + +describe("OrgPlacementOverrides", () => { + it("should parse a full override with nodeSelector and tolerations", () => { + expect( + OrgPlacementOverrides.parse( + JSON.stringify({ + org_123: { + nodeSelector: { "node.cluster.x-k8s.io/machinepool": "dedicated-pool" }, + tolerations: "dedicated=pool:NoSchedule", + }, + }) + ) + ).toEqual({ + org_123: { + nodeSelector: { "node.cluster.x-k8s.io/machinepool": "dedicated-pool" }, + tolerations: [{ key: "dedicated", operator: "Equal", value: "pool", effect: "NoSchedule" }], + }, + }); + }); + + it("should allow either half to be omitted", () => { + expect( + OrgPlacementOverrides.parse(JSON.stringify({ org_123: { nodeSelector: { pool: "a" } } })) + ).toEqual({ org_123: { nodeSelector: { pool: "a" } } }); + + expect( + OrgPlacementOverrides.parse(JSON.stringify({ org_123: { tolerations: "spot:NoExecute" } })) + ).toEqual({ + org_123: { tolerations: [{ key: "spot", operator: "Exists", effect: "NoExecute" }] }, + }); + + expect(OrgPlacementOverrides.parse(JSON.stringify({ org_123: {} }))).toEqual({ org_123: {} }); + }); + + it("should reject invalid JSON at startup rather than silently skipping the override", () => { + for (const invalid of ["not json", "[]", '"org_123"', "{"]) { + expect(OrgPlacementOverrides.safeParse(invalid).success).toBe(false); + } + }); + + it("should treat a blank or missing value as no overrides, like the sibling settings", () => { + expect(OrgPlacementOverrides.parse(undefined)).toBeUndefined(); + expect(OrgPlacementOverrides.parse("")).toBeUndefined(); + expect(OrgPlacementOverrides.parse(" ")).toBeUndefined(); + }); + + it("should accept tolerations as an array of entries, matching the Helm list shape", () => { + expect( + OrgPlacementOverrides.parse( + JSON.stringify({ + org_123: { tolerations: ["dedicated=pool:NoSchedule", "spot:NoExecute"] }, + }) + ) + ).toEqual({ + org_123: { + tolerations: [ + { key: "dedicated", operator: "Equal", value: "pool", effect: "NoSchedule" }, + { key: "spot", operator: "Exists", effect: "NoExecute" }, + ], + }, + }); + }); + + it("should coerce scalar node selector values to strings, as Kubernetes labels are", () => { + expect( + OrgPlacementOverrides.parse( + JSON.stringify({ org_123: { nodeSelector: { paid: true, replicas: 3 } } }) + ) + ).toEqual({ org_123: { nodeSelector: { paid: "true", replicas: "3" } } }); + }); + + it("should trim whitespace around node selector keys and values", () => { + expect( + OrgPlacementOverrides.parse( + JSON.stringify({ org_123: { nodeSelector: { " pool ": " a " } } }) + ) + ).toEqual({ org_123: { nodeSelector: { pool: "a" } } }); + }); + + it("should reject blank or padded org keys, since the lookup is exact", () => { + for (const key of [" ", " org_123", "org_123 "]) { + expect(OrgPlacementOverrides.safeParse(JSON.stringify({ [key]: {} })).success).toBe(false); + } + }); + + it("should reject an empty node selector value instead of pinning the org to nothing", () => { + for (const value of ["", " "]) { + expect( + OrgPlacementOverrides.safeParse( + JSON.stringify({ org_123: { nodeSelector: { pool: value } } }) + ).success + ).toBe(false); + } + }); + + it("should reject an unknown field, so a typo cannot silently drop an override", () => { + expect( + OrgPlacementOverrides.safeParse( + JSON.stringify({ org_123: { toleration: "dedicated=pool:NoSchedule" } }) + ).success + ).toBe(false); + }); + + it("should reject a node selector key or value Kubernetes would reject", () => { + for (const invalid of [ + { org_123: { nodeSelector: { "bad key": "a" } } }, + { org_123: { nodeSelector: { pool: "bad value" } } }, + { org_123: { nodeSelector: { "a/b/c": "a" } } }, + { org_123: { nodeSelector: { pool: "v".repeat(64) } } }, + ]) { + expect(OrgPlacementOverrides.safeParse(JSON.stringify(invalid)).success).toBe(false); + } + }); + + it("should reject an invalid toleration inside an override", () => { + expect( + OrgPlacementOverrides.safeParse( + JSON.stringify({ org_123: { tolerations: "dedicated=pool:Nope" } }) + ).success + ).toBe(false); + }); +}); diff --git a/apps/supervisor/src/envUtil.ts b/apps/supervisor/src/envUtil.ts index 67811f76fcb..e905141a43e 100644 --- a/apps/supervisor/src/envUtil.ts +++ b/apps/supervisor/src/envUtil.ts @@ -146,6 +146,102 @@ export const Tolerations = z.string().transform((val, ctx) => { }); }); +/** + * Scalar values are coerced: YAML/JSON easily produce `true` or `3` where a label + * value is meant, and Kubernetes label values are always strings. An empty value + * is rejected rather than passed through - as a selector it matches only nodes + * carrying a literal empty-valued label, which pins the org to nothing. + */ +const NodeSelector = z + .record(z.string(), z.union([z.string(), z.number(), z.boolean()])) + .transform((selector, ctx) => { + const result: Record = {}; + + for (const [rawKey, rawValue] of Object.entries(selector)) { + const key = rawKey.trim(); + const value = String(rawValue).trim(); + + if (!isQualifiedName(key)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid node selector key "${rawKey}". Must be a Kubernetes label key, optionally prefixed with a DNS subdomain.`, + }); + continue; + } + + if (!value) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Empty node selector value for key "${key}". Remove the key instead of blanking the value.`, + }); + continue; + } + + if (!isLabelValue(value)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid node selector value "${value}" for key "${key}". Must be a Kubernetes label value: alphanumeric, with dashes, underscores and dots inside, at most 63 characters.`, + }); + continue; + } + + result[key] = value; + } + + return result; + }); + +/** + * Per-organization placement overrides for run pods, as JSON keyed by the + * internal org id (the `org` label on run pods): + * `{"": {"nodeSelector": {"": ""}, "tolerations": ""}}`. + * Tolerations use the same CSV format as `Tolerations`, or an array of such + * entries. Everything is validated at startup for the same reason as + * tolerations above: a typo would otherwise reject every pod create for that + * org, with the cause buried in API errors. A blank value means no overrides. + */ +export const OrgPlacementOverrides = z + .string() + .optional() + .transform((val, ctx) => { + if (val === undefined || val.trim() === "") { + return undefined; + } + + try { + return JSON.parse(val) as unknown; + } catch { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid org placement overrides: not valid JSON", + }); + return z.NEVER; + } + }) + .pipe( + z + .record( + z + .string() + .min(1) + .refine((key) => key === key.trim() && key.trim().length > 0, { + message: + "Org override keys must not be blank or padded with whitespace; the lookup is exact", + }), + z + .object({ + nodeSelector: NodeSelector.optional(), + tolerations: z + .union([z.string(), z.array(z.string())]) + .transform((val) => (Array.isArray(val) ? val.join(",") : val)) + .pipe(Tolerations) + .optional(), + }) + .strict() + ) + .optional() + ); + export const AdditionalEnvVars = z.preprocess((val) => { if (typeof val !== "string") { return val; diff --git a/apps/supervisor/src/workloadManager/kubernetes.test.ts b/apps/supervisor/src/workloadManager/kubernetes.test.ts index bb15c23e9f4..3c6419e9c6f 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.test.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.test.ts @@ -4,6 +4,7 @@ import { nodetypeNodeSelector, runPodTolerations, withBlockIoUringSeccompProfile, + withNodeSelector, } from "./kubernetesPodSpec.js"; const basePodSpec = { @@ -54,6 +55,49 @@ describe("runPodTolerations", () => { expect(runPodTolerations(worker, [], true)).toEqual(worker); expect(runPodTolerations(worker, scheduled, true)).toEqual([...worker, ...scheduled]); }); + + it("appends the org tolerations regardless of run type", () => { + const org = [{ key: "dedicated", operator: "Equal", value: "org-pool", effect: "NoSchedule" }]; + + expect(runPodTolerations(undefined, undefined, false, org)).toEqual(org); + expect(runPodTolerations(worker, undefined, false, org)).toEqual([...worker, ...org]); + expect(runPodTolerations(worker, scheduled, true, org)).toEqual([ + ...worker, + ...scheduled, + ...org, + ]); + expect(runPodTolerations(undefined, undefined, false, [])).toBeUndefined(); + }); +}); + +describe("withNodeSelector", () => { + const podSpec = { ...basePodSpec, nodeSelector: { nodetype: "v4-worker", paid: "true" } }; + + it("returns the pod spec untouched when there is nothing to merge", () => { + expect(withNodeSelector(podSpec, undefined)).toBe(podSpec); + expect(withNodeSelector(podSpec, {})).toBe(podSpec); + }); + + it("merges extra entries with existing ones", () => { + expect(withNodeSelector(podSpec, { machinepool: "dedicated-pool" })).toEqual({ + ...podSpec, + nodeSelector: { nodetype: "v4-worker", paid: "true", machinepool: "dedicated-pool" }, + }); + }); + + it("lets the extra entries win on key collision", () => { + expect(withNodeSelector(podSpec, { nodetype: "other" }).nodeSelector).toEqual({ + nodetype: "other", + paid: "true", + }); + }); + + it("adds a nodeSelector to a spec that had none", () => { + expect(withNodeSelector(basePodSpec, { machinepool: "dedicated-pool" })).toEqual({ + ...basePodSpec, + nodeSelector: { machinepool: "dedicated-pool" }, + }); + }); }); describe("withBlockIoUringSeccompProfile", () => { diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index 1b88bafbc28..e0bf01a050f 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -18,6 +18,7 @@ import { nodetypeNodeSelector, runPodTolerations, withBlockIoUringSeccompProfile, + withNodeSelector, } from "./kubernetesPodSpec.js"; type ResourceQuantities = { @@ -69,6 +70,12 @@ export class KubernetesWorkloadManager implements WorkloadManager { domain: opts.workloadApiDomain, }); } + + if (env.KUBERNETES_ORG_PLACEMENT_OVERRIDES) { + this.logger.info("[KubernetesWorkloadManager] Org placement overrides enabled", { + orgIds: Object.keys(env.KUBERNETES_ORG_PLACEMENT_OVERRIDES), + }); + } } private addPlacementTags( @@ -110,7 +117,24 @@ export class KubernetesWorkloadManager implements WorkloadManager { const runnerId = getRunnerId(opts.runFriendlyId, opts.nextAttemptNumber); try { - const basePodSpec = this.addPlacementTags(this.#defaultPodSpec, opts.placementTags); + const orgOverride = env.KUBERNETES_ORG_PLACEMENT_OVERRIDES?.[opts.orgId]; + const taggedPodSpec = this.addPlacementTags(this.#defaultPodSpec, opts.placementTags); + const basePodSpec = withNodeSelector(taggedPodSpec, orgOverride?.nodeSelector); + + if (orgOverride?.nodeSelector) { + const replacedKeys = Object.keys(orgOverride.nodeSelector).filter( + (key) => + taggedPodSpec.nodeSelector?.[key] !== undefined && + taggedPodSpec.nodeSelector[key] !== orgOverride.nodeSelector?.[key] + ); + + if (replacedKeys.length > 0) { + this.logger.warn( + "[KubernetesWorkloadManager] Org placement override replaces node selector keys", + { orgId: opts.orgId, replacedKeys } + ); + } + } const podSpec = this.opts.checkpointsEnabled ? withBlockIoUringSeccompProfile(basePodSpec, opts.runtime) : basePodSpec; @@ -131,7 +155,7 @@ export class KubernetesWorkloadManager implements WorkloadManager { spec: { ...podSpec, affinity: this.#getAffinity(opts), - tolerations: this.#getTolerations(this.#isScheduledRun(opts)), + tolerations: this.#getTolerations(this.#isScheduledRun(opts), orgOverride?.tolerations), terminationGracePeriodSeconds: 60 * 60, containers: [ { @@ -555,11 +579,15 @@ export class KubernetesWorkloadManager implements WorkloadManager { }; } - #getTolerations(isScheduledRun: boolean): k8s.V1Toleration[] | undefined { + #getTolerations( + isScheduledRun: boolean, + orgTolerations?: k8s.V1Toleration[] + ): k8s.V1Toleration[] | undefined { return runPodTolerations( env.KUBERNETES_RUNNER_TOLERATIONS, env.KUBERNETES_SCHEDULED_RUN_TOLERATIONS, - isScheduledRun + isScheduledRun, + orgTolerations ); } diff --git a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts index ba15e563f6d..f521369f082 100644 --- a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts +++ b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts @@ -19,23 +19,47 @@ export function nodetypeNodeSelector( /** * Tolerations for a run pod: the cluster-wide set, plus the scheduled-run set when the - * run came from a schedule tree. Not reconciled - Kubernetes matches tolerations as an - * any-match set, so a broad entry in one set can subsume a narrower one in the other. + * run came from a schedule tree, plus the org's own set when a placement override + * matches. Not reconciled - Kubernetes matches tolerations as an any-match set, so a + * broad entry in one set can subsume a narrower one in another. * Returns undefined rather than an empty array to leave the field unset. */ export function runPodTolerations( runnerTolerations: k8s.V1Toleration[] | undefined, scheduledRunTolerations: k8s.V1Toleration[] | undefined, - isScheduledRun: boolean + isScheduledRun: boolean, + orgTolerations?: k8s.V1Toleration[] ): k8s.V1Toleration[] | undefined { const tolerations = [ ...(runnerTolerations ?? []), ...(isScheduledRun ? (scheduledRunTolerations ?? []) : []), + ...(orgTolerations ?? []), ]; return tolerations.length > 0 ? tolerations : undefined; } +/** + * Merges extra node selector entries into a pod spec. Later entries win on key + * collision, so an override can retarget a key set by an earlier stage. + */ +export function withNodeSelector( + podSpec: Omit, + nodeSelector: Record | undefined +): Omit { + if (!nodeSelector || Object.keys(nodeSelector).length === 0) { + return podSpec; + } + + return { + ...podSpec, + nodeSelector: { + ...podSpec.nodeSelector, + ...nodeSelector, + }, + }; +} + /** * Node >= 24 always creates io_uring fds, which can't be checkpointed. Blocking * io_uring_setup makes libuv fall back to epoll. Other runtimes don't need this, diff --git a/docs/self-hosting/env/supervisor.mdx b/docs/self-hosting/env/supervisor.mdx index a7e4ef96692..92cfe100d65 100644 --- a/docs/self-hosting/env/supervisor.mdx +++ b/docs/self-hosting/env/supervisor.mdx @@ -48,6 +48,7 @@ mode: "wide" | `KUBERNETES_NAMESPACE` | No | default | The namespace that runs should be in. | | `KUBERNETES_WORKER_NODETYPE_LABEL` | No | v4-worker | Nodes for runs need `nodetype=`. Empty: any node. | | `KUBERNETES_RUNNER_TOLERATIONS` | No | — | Run pod tolerations. CSV: `key=value:effect`/`key:effect`. | +| `KUBERNETES_ORG_PLACEMENT_OVERRIDES` | No | — | Per-org run pod placement. JSON keyed by internal org ID. | | `KUBERNETES_IMAGE_PULL_SECRETS` | No | — | Image pull secrets (CSV). | | `KUBERNETES_EPHEMERAL_STORAGE_SIZE_LIMIT` | No | 10Gi | Ephemeral storage size limit. Applies to all runs. | | `KUBERNETES_EPHEMERAL_STORAGE_SIZE_REQUEST` | No | 2Gi | Ephemeral storage size request. Applies to all runs. | diff --git a/hosting/k8s/helm/templates/supervisor.yaml b/hosting/k8s/helm/templates/supervisor.yaml index 84a6350f035..59b797c53af 100644 --- a/hosting/k8s/helm/templates/supervisor.yaml +++ b/hosting/k8s/helm/templates/supervisor.yaml @@ -174,6 +174,10 @@ spec: - name: KUBERNETES_RUNNER_TOLERATIONS value: {{ join "," . | quote }} {{- end }} + {{- with .Values.supervisor.config.kubernetes.orgPlacementOverrides }} + - name: KUBERNETES_ORG_PLACEMENT_OVERRIDES + value: {{ toJson . | quote }} + {{- end }} {{- $registryAuthEnabled := false }} {{- if .Values.registry.deploy }} {{- $registryAuthEnabled = .Values.registry.auth.enabled }} diff --git a/hosting/k8s/helm/values.yaml b/hosting/k8s/helm/values.yaml index 354d8e55ba1..8e7229bf9a3 100644 --- a/hosting/k8s/helm/values.yaml +++ b/hosting/k8s/helm/values.yaml @@ -297,6 +297,9 @@ supervisor: namespace: "" # Default: uses release namespace workerNodetypeLabel: "" # When set, runs will only be scheduled on nodes with "nodetype=
); } diff --git a/apps/webapp/app/routes/admin.api.v1.logs-search-projector.ts b/apps/webapp/app/routes/admin.api.v1.logs-search-projector.ts new file mode 100644 index 00000000000..7c8da6e3c27 --- /dev/null +++ b/apps/webapp/app/routes/admin.api.v1.logs-search-projector.ts @@ -0,0 +1,8 @@ +import { type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; +import { getLogsSearchProjector } from "~/services/logsSearchProjectorInstance.server"; +import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; + +export async function loader({ request }: LoaderFunctionArgs) { + await requireAdminApiRequest(request); + return json(await getLogsSearchProjector().status()); +} diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.can-view-logs-page/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.can-view-logs-page/route.tsx index 501b4a8ad35..5412cb5a900 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.can-view-logs-page/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.can-view-logs-page/route.tsx @@ -1,43 +1,9 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson } from "remix-typedjson"; import { requireUser } from "~/services/session.server"; -import { prisma } from "~/db.server"; -import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags"; +import { hasLogsPageAccess } from "~/services/logsAccess.server"; import { OrganizationParamsSchema } from "~/utils/pathBuilder"; -async function hasLogsPageAccess( - userId: string, - isAdmin: boolean, - isImpersonating: boolean, - organizationSlug: string -): Promise { - if (isAdmin || isImpersonating) { - return true; - } - - const organization = await prisma.organization.findFirst({ - where: { - slug: organizationSlug, - members: { some: { userId } }, - }, - select: { - featureFlags: true, - }, - }); - - if (!organization?.featureFlags) { - return false; - } - - const flags = organization.featureFlags as Record; - const hasLogsPageAccessResult = validateFeatureFlagValue( - FEATURE_FLAG.hasLogsPageAccess, - flags.hasLogsPageAccess - ); - - return hasLogsPageAccessResult.success && hasLogsPageAccessResult.data === true; -} - export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); const { organizationSlug } = OrganizationParamsSchema.parse(params); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId.tsx index 418cee805c1..fcc607b6732 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId.tsx @@ -2,7 +2,7 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson } from "remix-typedjson"; import { z } from "zod"; import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; -import { requireUserId } from "~/services/session.server"; +import { requireUser } from "~/services/session.server"; import { LogDetailPresenter } from "~/presenters/v3/LogDetailPresenter.server"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; @@ -10,6 +10,7 @@ import { $replica } from "~/db.server"; import { runStore } from "~/v3/runStore.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import type { TaskRunStatus } from "@trigger.dev/database"; +import { hasLogsPageAccess } from "~/services/logsAccess.server"; const LogIdParamsSchema = z.object({ organizationSlug: z.string(), @@ -19,9 +20,14 @@ const LogIdParamsSchema = z.object({ }); export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const userId = await requireUserId(request); + const user = await requireUser(request); + const userId = user.id; const { organizationSlug, projectParam, envParam, logId } = LogIdParamsSchema.parse(params); + if (!(await hasLogsPageAccess(user.id, user.admin, user.isImpersonating, organizationSlug))) { + throw new Response("Logs are not available", { status: 403 }); + } + // Validate access to project and environment const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.ts index a3425bd2dab..8acec2ad634 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.ts @@ -12,6 +12,8 @@ import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstan import { getCurrentPlan } from "~/services/platform.v3.server"; import { requireUser } from "~/services/session.server"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; +import { hasLogsPageAccess } from "~/services/logsAccess.server"; +import { ServiceValidationError } from "~/v3/services/baseService.server"; // Valid log levels for filtering const validLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]; @@ -27,6 +29,9 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = user.id; const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params); + if (!(await hasLogsPageAccess(user.id, user.admin, user.isImpersonating, organizationSlug))) { + throw new Response("Logs are not available", { status: 403 }); + } const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { @@ -69,7 +74,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { from, to, levels, - defaultPeriod: "1h", + defaultPeriod: "1d", retentionLimitDays, }) as any; // Validated by LogsListOptionsSchema at runtime @@ -78,7 +83,16 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { "logs" ); const presenter = new LogsListPresenter($replica, logsClickhouse); - const result = await presenter.call(project.organizationId, environment.id, options); + + let result; + try { + result = await presenter.call(project.organizationId, environment.id, options); + } catch (error) { + if (error instanceof ServiceValidationError) { + throw new Response(error.message, { status: error.status ?? 422 }); + } + throw error; + } return json({ logs: result.logs, diff --git a/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts b/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts index ce087279cfa..5a2b3b86eee 100644 --- a/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts +++ b/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts @@ -37,6 +37,24 @@ const defaultLogsClickhouseClient = singleton( initializeLogsClickhouseClient ); +function initializeLogsSearchProjectorClickhouseClient() { + const url = new URL(env.LOGS_CLICKHOUSE_URL ?? env.CLICKHOUSE_URL); + url.searchParams.delete("secure"); + + return new ClickHouse({ + url: url.toString(), + name: "logs-search-projector", + keepAlive: { + enabled: env.CLICKHOUSE_KEEP_ALIVE_ENABLED === "1", + idleSocketTtl: env.CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS, + }, + logLevel: env.CLICKHOUSE_LOG_LEVEL, + compression: { request: true }, + maxOpenConnections: Math.min(env.CLICKHOUSE_MAX_OPEN_CONNECTIONS, 2), + requestTimeoutMs: (env.LOGS_SEARCH_PROJECTOR_MAX_EXECUTION_TIME_SECONDS + 30) * 1000, + }); +} + function getLogsListClickhouseSettings() { return { max_memory_usage: env.CLICKHOUSE_LOGS_LIST_MAX_MEMORY_USAGE.toString(), @@ -62,11 +80,7 @@ function getLogsListClickhouseSettings() { } function initializeLogsClickhouseClient() { - if (!env.LOGS_CLICKHOUSE_URL) { - throw new Error("LOGS_CLICKHOUSE_URL is not set"); - } - - const url = new URL(env.LOGS_CLICKHOUSE_URL); + const url = new URL(env.LOGS_CLICKHOUSE_URL ?? env.CLICKHOUSE_READER_URL ?? env.CLICKHOUSE_URL); url.searchParams.delete("secure"); return new ClickHouse({ @@ -678,6 +692,13 @@ export function getAdminClickhouse(): ClickHouse { return defaultAdminClickhouseClient; } +export function getLogsSearchProjectorClickhouseClient(): ClickHouse { + return singleton( + "logsSearchProjectorClickhouseClient", + initializeLogsSearchProjectorClickhouseClient + ); +} + /** Queue-metrics client for callers with no organization in scope (the ingestion consumer). */ export function getQueueMetricsClickhouseClient(): ClickHouse { return defaultQueueMetricsClickhouseClient; diff --git a/apps/webapp/app/services/logsAccess.server.ts b/apps/webapp/app/services/logsAccess.server.ts new file mode 100644 index 00000000000..35cd2ee1d63 --- /dev/null +++ b/apps/webapp/app/services/logsAccess.server.ts @@ -0,0 +1,29 @@ +import { prisma } from "~/db.server"; +import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags"; + +export async function hasLogsPageAccess( + userId: string, + isAdmin: boolean, + isImpersonating: boolean, + organizationSlug: string +): Promise { + if (isAdmin || isImpersonating) { + return true; + } + + const organization = await prisma.organization.findFirst({ + where: { + slug: organizationSlug, + members: { some: { userId } }, + }, + select: { featureFlags: true }, + }); + + if (!organization?.featureFlags) { + return false; + } + + const flags = organization.featureFlags as Record; + const result = validateFeatureFlagValue(FEATURE_FLAG.hasLogsPageAccess, flags.hasLogsPageAccess); + return result.success && result.data === true; +} diff --git a/apps/webapp/app/services/logsSearchProjector.server.ts b/apps/webapp/app/services/logsSearchProjector.server.ts new file mode 100644 index 00000000000..1dee6fd0b5b --- /dev/null +++ b/apps/webapp/app/services/logsSearchProjector.server.ts @@ -0,0 +1,409 @@ +import { randomUUID } from "node:crypto"; +import { logger as defaultLogger } from "~/services/logger.server"; +import { + logsSearchProjectorTelemetry, + type LogsSearchProjectionMode, +} from "~/services/logsSearchProjectorTelemetry.server"; + +export type { LogsSearchProjectionMode } from "~/services/logsSearchProjectorTelemetry.server"; + +export const LOGS_SEARCH_PROJECTOR_ID = "task_events_search_v2"; +export const LOGS_SEARCH_PROJECTOR_INITIAL_MODE = "INITIAL"; +export const LOGS_SEARCH_PROJECTOR_CHECKPOINT_MODE = "FINALIZED"; +const LOGS_SEARCH_PREVIEW_WINDOW_MS = 5_000; +const LOGS_SEARCH_PREVIEW_SAFETY_DELAY_MS = 2_000; +const LOGS_SEARCH_FINALIZED_WINDOW_MS = 60_000; +const LOGS_SEARCH_FINALIZED_SAFETY_DELAY_MS = 120_000; + +export type LogsSearchProjectorWindow = { + mode: LogsSearchProjectionMode; + start: Date; + end: Date; +}; + +export type LogsSearchProjectorProjectionResult = { + queryId: string; + readRows: number; + writtenRows: number; +}; + +export type LogsSearchProjectorCheckpointResult = "inserted" | "duplicate"; + +export type LogsSearchProjectorLeaseStatus = { + mode: LogsSearchProjectionMode; + expiresAt: Date; +}; + +export type LogsSearchProjectorStatus = { + initialized: boolean; + enabled: boolean; + previewEnabled: boolean; + previewWatermark: Date | null; + previewSafeCutoff: Date | null; + previewLagMs: number | null; + finalizedWatermark: Date | null; + finalizedSafeCutoff: Date | null; + finalizedLagMs: number | null; + finalizedWindowsDue: number | null; + activeProjectionMode: LogsSearchProjectionMode | null; + leaseExpiresAt: Date | null; +}; + +export type LogsSearchProjectorConfig = { + enabled: boolean; + previewEnabled: boolean; + maxFinalizedWindowsPerTick: number; + leaseDurationMs: number; +}; + +export type LogsSearchProjectorStateStore = { + initialize(initialWatermark: Date): Promise; + findInitialWatermark(): Promise; + getFinalizedWatermark(initialWatermark: Date): Promise; + appendFinalizedCheckpoint( + window: LogsSearchProjectorWindow, + result: LogsSearchProjectorProjectionResult + ): Promise; +}; + +export type LogsSearchProjectorRedisStore = { + acquireLease(token: string, mode: LogsSearchProjectionMode, durationMs: number): Promise; + releaseLease(token: string, mode: LogsSearchProjectionMode): Promise; + readLeaseStatus(): Promise; + initializePreviewWatermark(boundary: Date): Promise; + getPreviewWatermark(): Promise; + advancePreviewWatermark(next: Date): Promise; +}; + +export class LogsSearchProjector { + constructor( + private readonly config: LogsSearchProjectorConfig, + private readonly stateStore: LogsSearchProjectorStateStore, + private readonly redisStore: LogsSearchProjectorRedisStore, + private readonly projectWindow: ( + window: LogsSearchProjectorWindow + ) => Promise, + private readonly clock: () => Date | Promise = () => new Date(), + private readonly logger: Pick< + typeof defaultLogger, + "debug" | "info" | "warn" | "error" + > = defaultLogger + ) {} + + async processTick(): Promise<{ finalized: number; preview: boolean }> { + if (!this.config.enabled) return { finalized: 0, preview: false }; + + const now = await this.clock(); + const finalizedCutoff = finalizedSafeCutoff(now); + const initialWatermark = await this.stateStore.initialize(finalizedCutoff); + + let finalized: number; + try { + finalized = await this.processFinalizedWindows(finalizedCutoff, initialWatermark); + } catch (error) { + await this.updateTelemetryStateAfterFailure(now, initialWatermark); + throw error; + } + + const currentNow = await this.clock(); + const currentFinalizedCutoff = finalizedSafeCutoff(currentNow); + const finalizedWatermark = await this.stateStore.getFinalizedWatermark(initialWatermark); + + let preview = false; + if (this.config.previewEnabled && finalizedWatermark >= currentFinalizedCutoff) { + preview = await this.processPreviewWindow(previewSafeCutoff(currentNow)); + } + + await this.updateTelemetryState(currentNow, initialWatermark); + return { finalized, preview }; + } + + async status(): Promise { + const initialWatermark = await this.stateStore.findInitialWatermark(); + if (!initialWatermark) return uninitializedProjectorStatus(this.config); + + let now: Date | null = null; + try { + now = await this.clock(); + } catch (error) { + this.logger.warn("Failed to read ClickHouse clock for logs search projector status", { + error, + }); + } + + return this.buildStatus(initialWatermark, now); + } + + private async processFinalizedWindows(cutoff: Date, initialWatermark: Date): Promise { + let processed = 0; + + for (let index = 0; index < this.config.maxFinalizedWindowsPerTick; index++) { + const token = randomUUID(); + const acquired = await this.redisStore.acquireLease( + token, + "finalized", + this.config.leaseDurationMs + ); + if (!acquired) { + logsSearchProjectorTelemetry.recordLeaseContention("finalized"); + break; + } + + let shouldStop = false; + try { + const watermark = await this.stateStore.getFinalizedWatermark(initialWatermark); + const window = selectFinalizedWindow(watermark, cutoff); + if (!window) break; + + const result = await this.project(window); + const checkpoint = await this.stateStore.appendFinalizedCheckpoint(window, result); + if (checkpoint === "duplicate") { + logsSearchProjectorTelemetry.recordCheckpointConflict(); + this.logger.warn("Logs search finalized checkpoint already exists", { + windowStart: window.start, + windowEnd: window.end, + queryId: result.queryId, + }); + shouldStop = true; + } else { + processed++; + this.logger.info("Projected finalized logs search window", { + windowStart: window.start, + windowEnd: window.end, + queryId: result.queryId, + readRows: result.readRows, + writtenRows: result.writtenRows, + }); + } + } finally { + await this.releaseLease(token, "finalized"); + } + + if (shouldStop) break; + } + + return processed; + } + + private async processPreviewWindow(cutoff: Date): Promise { + const token = randomUUID(); + const acquired = await this.redisStore.acquireLease( + token, + "preview", + this.config.leaseDurationMs + ); + if (!acquired) { + logsSearchProjectorTelemetry.recordLeaseContention("preview"); + return false; + } + + try { + const watermark = await this.redisStore.initializePreviewWatermark(cutoff); + const selection = selectPreviewWindow(watermark, cutoff); + if (!selection.window) return false; + + if (selection.skippedWindows > 0) { + await this.redisStore.advancePreviewWatermark(selection.window.start); + logsSearchProjectorTelemetry.recordPreviewSkipped(selection.skippedWindows); + this.logger.warn("Skipped stale logs search preview windows", { + skippedWindows: selection.skippedWindows, + previousWatermark: watermark, + nextWindowStart: selection.window.start, + }); + } + + try { + const result = await this.project(selection.window); + await this.redisStore.advancePreviewWatermark(selection.window.end); + this.logger.debug("Projected preview logs search window", { + windowStart: selection.window.start, + windowEnd: selection.window.end, + queryId: result.queryId, + readRows: result.readRows, + writtenRows: result.writtenRows, + }); + return true; + } catch (error) { + this.logger.error("Logs search preview projection failed", { + error, + windowStart: selection.window.start, + windowEnd: selection.window.end, + }); + return false; + } + } finally { + await this.releaseLease(token, "preview"); + } + } + + private async project( + window: LogsSearchProjectorWindow + ): Promise { + const startedAt = Date.now(); + try { + const result = await this.projectWindow(window); + logsSearchProjectorTelemetry.recordWindow( + window.mode, + "success", + Date.now() - startedAt, + result.readRows, + result.writtenRows + ); + return result; + } catch (error) { + logsSearchProjectorTelemetry.recordWindow(window.mode, "error", Date.now() - startedAt); + if (window.mode === "finalized") { + this.logger.error("Logs search finalized projection failed", { + error, + windowStart: window.start, + windowEnd: window.end, + }); + } + throw error; + } + } + + private async releaseLease(token: string, mode: LogsSearchProjectionMode): Promise { + try { + await this.redisStore.releaseLease(token, mode); + } catch (error) { + this.logger.warn("Failed to release logs search projector lease", { error, mode }); + } + } + + private async buildStatus( + initialWatermark: Date, + now: Date | null + ): Promise { + const finalizedWatermark = await this.stateStore.getFinalizedWatermark(initialWatermark); + let previewWatermark: Date | null = null; + let lease: LogsSearchProjectorLeaseStatus | null = null; + + try { + [previewWatermark, lease] = await Promise.all([ + this.redisStore.getPreviewWatermark(), + this.redisStore.readLeaseStatus(), + ]); + } catch (error) { + this.logger.warn("Failed to read Redis logs search projector status", { error }); + } + + const previewCutoff = now ? previewSafeCutoff(now) : null; + const finalizedCutoff = now ? finalizedSafeCutoff(now) : null; + const previewLagMs = lagMs(previewWatermark, previewCutoff); + const finalizedLagMs = lagMs(finalizedWatermark, finalizedCutoff); + + return { + initialized: true, + enabled: this.config.enabled, + previewEnabled: this.config.previewEnabled, + previewWatermark, + previewSafeCutoff: previewCutoff, + previewLagMs, + finalizedWatermark, + finalizedSafeCutoff: finalizedCutoff, + finalizedLagMs, + finalizedWindowsDue: + finalizedLagMs === null + ? null + : Math.floor(finalizedLagMs / LOGS_SEARCH_FINALIZED_WINDOW_MS), + activeProjectionMode: lease?.mode ?? null, + leaseExpiresAt: lease?.expiresAt ?? null, + }; + } + + private async updateTelemetryStateAfterFailure(now: Date, initialWatermark: Date): Promise { + try { + await this.updateTelemetryState(now, initialWatermark); + } catch (error) { + this.logger.warn("Failed to refresh logs search projector telemetry after tick failure", { + error, + }); + } + } + + private async updateTelemetryState(now: Date, initialWatermark: Date): Promise { + const status = await this.buildStatus(initialWatermark, now); + logsSearchProjectorTelemetry.updateState({ + previewLagMs: status.previewLagMs, + finalizedLagMs: status.finalizedLagMs ?? 0, + }); + } +} + +function calculateClosedWindowBoundary(now: Date, safetyDelayMs: number, windowMs: number): Date { + return new Date(Math.floor((now.getTime() - safetyDelayMs) / windowMs) * windowMs); +} + +export function previewSafeCutoff(now: Date): Date { + return calculateClosedWindowBoundary( + now, + LOGS_SEARCH_PREVIEW_SAFETY_DELAY_MS, + LOGS_SEARCH_PREVIEW_WINDOW_MS + ); +} + +export function finalizedSafeCutoff(now: Date): Date { + return calculateClosedWindowBoundary( + now, + LOGS_SEARCH_FINALIZED_SAFETY_DELAY_MS, + LOGS_SEARCH_FINALIZED_WINDOW_MS + ); +} + +export function selectFinalizedWindow( + watermark: Date, + safeCutoff: Date +): LogsSearchProjectorWindow | null { + if (watermark >= safeCutoff) return null; + return { + mode: "finalized", + start: watermark, + end: new Date(watermark.getTime() + LOGS_SEARCH_FINALIZED_WINDOW_MS), + }; +} + +export function selectPreviewWindow( + watermark: Date, + safeCutoff: Date +): { window: LogsSearchProjectorWindow | null; skippedWindows: number } { + if (watermark >= safeCutoff) return { window: null, skippedWindows: 0 }; + + const latestWindowStart = new Date(safeCutoff.getTime() - LOGS_SEARCH_PREVIEW_WINDOW_MS); + const start = watermark < latestWindowStart ? latestWindowStart : watermark; + return { + window: { + mode: "preview", + start, + end: new Date(start.getTime() + LOGS_SEARCH_PREVIEW_WINDOW_MS), + }, + skippedWindows: Math.max( + 0, + Math.floor((start.getTime() - watermark.getTime()) / LOGS_SEARCH_PREVIEW_WINDOW_MS) + ), + }; +} + +function lagMs(watermark: Date | null, cutoff: Date | null): number | null { + if (!watermark || !cutoff) return null; + return Math.max(0, cutoff.getTime() - watermark.getTime()); +} + +function uninitializedProjectorStatus( + config: LogsSearchProjectorConfig +): LogsSearchProjectorStatus { + return { + initialized: false, + enabled: config.enabled, + previewEnabled: config.previewEnabled, + previewWatermark: null, + previewSafeCutoff: null, + previewLagMs: null, + finalizedWatermark: null, + finalizedSafeCutoff: null, + finalizedLagMs: null, + finalizedWindowsDue: null, + activeProjectionMode: null, + leaseExpiresAt: null, + }; +} diff --git a/apps/webapp/app/services/logsSearchProjectorInstance.server.ts b/apps/webapp/app/services/logsSearchProjectorInstance.server.ts new file mode 100644 index 00000000000..1d231968784 --- /dev/null +++ b/apps/webapp/app/services/logsSearchProjectorInstance.server.ts @@ -0,0 +1,64 @@ +import { z } from "zod"; +import { prisma } from "~/db.server"; +import { env } from "~/env.server"; +import { createRedisClient } from "~/redis.server"; +import { getLogsSearchProjectorClickhouseClient } from "~/services/clickhouse/clickhouseFactory.server"; +import { LogsSearchProjector } from "~/services/logsSearchProjector.server"; +import { RedisLogsSearchProjectorStore } from "~/services/logsSearchProjectorRedisStore.server"; +import { PrismaLogsSearchProjectorStateStore } from "~/services/logsSearchProjectorStateStore.server"; +import { singleton } from "~/utils/singleton"; + +function initializeLogsSearchProjector() { + const clickhouse = getLogsSearchProjectorClickhouseClient(); + const serverClockQuery = clickhouse.reader.query({ + name: "get-logs-search-projector-clock", + query: "SELECT toUnixTimestamp64Milli(now64(3)) AS now_ms", + schema: z.object({ now_ms: z.number().or(z.string()) }), + }); + const limits = { + maxExecutionTimeSeconds: env.LOGS_SEARCH_PROJECTOR_MAX_EXECUTION_TIME_SECONDS, + maxRowsToRead: env.LOGS_SEARCH_PROJECTOR_MAX_ROWS_TO_READ, + maxMemoryUsage: env.LOGS_SEARCH_PROJECTOR_MAX_MEMORY_USAGE, + maxThreads: env.LOGS_SEARCH_PROJECTOR_MAX_THREADS, + }; + const redis = createRedisClient("logs-search-projector", { + host: env.COMMON_WORKER_REDIS_HOST, + port: env.COMMON_WORKER_REDIS_PORT, + username: env.COMMON_WORKER_REDIS_USERNAME, + password: env.COMMON_WORKER_REDIS_PASSWORD, + tlsDisabled: env.COMMON_WORKER_REDIS_TLS_DISABLED === "true", + clusterMode: env.COMMON_WORKER_REDIS_CLUSTER_MODE_ENABLED === "1", + maxRetriesPerRequest: 2, + }); + + return new LogsSearchProjector( + { + enabled: env.LOGS_SEARCH_PROJECTOR_ENABLED, + previewEnabled: env.LOGS_SEARCH_PROJECTOR_PREVIEW_ENABLED, + maxFinalizedWindowsPerTick: env.LOGS_SEARCH_PROJECTOR_MAX_WINDOWS_PER_TICK, + leaseDurationMs: (env.LOGS_SEARCH_PROJECTOR_MAX_EXECUTION_TIME_SECONDS + 90) * 1000, + }, + new PrismaLogsSearchProjectorStateStore(prisma), + new RedisLogsSearchProjectorStore(redis), + async (window) => { + const [error, result] = await clickhouse.taskEventsSearch.projectV2Window(window, limits); + if (error) throw error; + return { + queryId: result.query_id, + readRows: Number(result.summary?.read_rows ?? 0), + writtenRows: Number(result.summary?.written_rows ?? 0), + }; + }, + async () => { + const [error, rows] = await serverClockQuery({}); + if (error) throw error; + const nowMs = Number(rows[0]?.now_ms); + if (!Number.isFinite(nowMs)) throw new Error("ClickHouse returned an invalid server clock"); + return new Date(nowMs); + } + ); +} + +export function getLogsSearchProjector() { + return singleton("logsSearchProjector", initializeLogsSearchProjector); +} diff --git a/apps/webapp/app/services/logsSearchProjectorRedisStore.server.ts b/apps/webapp/app/services/logsSearchProjectorRedisStore.server.ts new file mode 100644 index 00000000000..7b2e007830c --- /dev/null +++ b/apps/webapp/app/services/logsSearchProjectorRedisStore.server.ts @@ -0,0 +1,112 @@ +import type { RedisClient } from "~/redis.server"; +import type { + LogsSearchProjectionMode, + LogsSearchProjectorLeaseStatus, + LogsSearchProjectorRedisStore, +} from "~/services/logsSearchProjector.server"; + +const LEASE_KEY = "logs-search-projector:coordination:lease"; +const PREVIEW_WATERMARK_KEY = "logs-search-projector:coordination:preview-watermark"; + +const releaseLeaseScript = ` + if redis.call("GET", KEYS[1]) == ARGV[1] then + return redis.call("DEL", KEYS[1]) + end + return 0 +`; + +const advanceWatermarkScript = ` + local current = redis.call("GET", KEYS[1]) + if not current or tonumber(ARGV[1]) > tonumber(current) then + redis.call("SET", KEYS[1], ARGV[1]) + return 1 + end + return 0 +`; + +type LeaseValue = { + token: string; + mode: LogsSearchProjectionMode; +}; + +export class RedisLogsSearchProjectorStore implements LogsSearchProjectorRedisStore { + constructor(private readonly redis: RedisClient) {} + + async acquireLease( + token: string, + mode: LogsSearchProjectionMode, + durationMs: number + ): Promise { + const result = await this.redis.set( + LEASE_KEY, + serializeLease({ token, mode }), + "PX", + durationMs, + "NX" + ); + return result === "OK"; + } + + async releaseLease(token: string, mode: LogsSearchProjectionMode): Promise { + await this.redis.eval(releaseLeaseScript, 1, LEASE_KEY, serializeLease({ token, mode })); + } + + async readLeaseStatus(): Promise { + const [value, ttlMs] = await Promise.all([ + this.redis.get(LEASE_KEY), + this.redis.pttl(LEASE_KEY), + ]); + if (!value || ttlMs < 0) return null; + + const lease = parseLease(value); + if (!lease) return null; + return { mode: lease.mode, expiresAt: new Date(Date.now() + ttlMs) }; + } + + async initializePreviewWatermark(boundary: Date): Promise { + await this.redis.set(PREVIEW_WATERMARK_KEY, boundary.getTime().toString(), "NX"); + const watermark = await this.getPreviewWatermark(); + if (!watermark) throw new Error("Failed to initialize logs search preview watermark"); + return watermark; + } + + async getPreviewWatermark(): Promise { + const value = await this.redis.get(PREVIEW_WATERMARK_KEY); + if (value === null) return null; + + const timestamp = Number(value); + if (!Number.isFinite(timestamp)) { + throw new Error("Logs search preview watermark is invalid"); + } + return new Date(timestamp); + } + + async advancePreviewWatermark(next: Date): Promise { + const result = await this.redis.eval( + advanceWatermarkScript, + 1, + PREVIEW_WATERMARK_KEY, + next.getTime().toString() + ); + return Number(result) === 1; + } +} + +function serializeLease(value: LeaseValue): string { + return JSON.stringify(value); +} + +function parseLease(value: string): LeaseValue | null { + try { + const parsed = JSON.parse(value) as Partial; + if ( + typeof parsed.token !== "string" || + (parsed.mode !== "preview" && parsed.mode !== "finalized") + ) { + return null; + } + return { token: parsed.token, mode: parsed.mode }; + } catch { + return null; + } +} diff --git a/apps/webapp/app/services/logsSearchProjectorStateStore.server.ts b/apps/webapp/app/services/logsSearchProjectorStateStore.server.ts new file mode 100644 index 00000000000..02ea3766d11 --- /dev/null +++ b/apps/webapp/app/services/logsSearchProjectorStateStore.server.ts @@ -0,0 +1,83 @@ +import type { PrismaClient } from "@trigger.dev/database"; +import { + LOGS_SEARCH_PROJECTOR_CHECKPOINT_MODE, + LOGS_SEARCH_PROJECTOR_ID, + LOGS_SEARCH_PROJECTOR_INITIAL_MODE, + type LogsSearchProjectorCheckpointResult, + type LogsSearchProjectorProjectionResult, + type LogsSearchProjectorStateStore, + type LogsSearchProjectorWindow, +} from "~/services/logsSearchProjector.server"; + +type LogsSearchProjectorDatabase = Pick; + +export class PrismaLogsSearchProjectorStateStore implements LogsSearchProjectorStateStore { + constructor(private readonly database: LogsSearchProjectorDatabase) {} + + async initialize(initialWatermark: Date): Promise { + const existing = await this.findInitialWatermark(); + if (existing) return existing; + + await this.database.logsSearchProjectorCheckpoint.createMany({ + data: [ + { + projectorId: LOGS_SEARCH_PROJECTOR_ID, + mode: LOGS_SEARCH_PROJECTOR_INITIAL_MODE, + windowStart: initialWatermark, + windowEnd: initialWatermark, + }, + ], + skipDuplicates: true, + }); + + // Concurrent first ticks can choose adjacent safe boundaries. Starting from the earliest + // append-only initialization checkpoint preserves complete forward coverage. + return (await this.findInitialWatermark()) ?? initialWatermark; + } + + async findInitialWatermark(): Promise { + const checkpoint = await this.database.logsSearchProjectorCheckpoint.findFirst({ + where: { + projectorId: LOGS_SEARCH_PROJECTOR_ID, + mode: LOGS_SEARCH_PROJECTOR_INITIAL_MODE, + }, + orderBy: { windowEnd: "asc" }, + select: { windowEnd: true }, + }); + + return checkpoint?.windowEnd ?? null; + } + + async getFinalizedWatermark(initialWatermark: Date): Promise { + const checkpoint = await this.database.logsSearchProjectorCheckpoint.findFirst({ + where: { + projectorId: LOGS_SEARCH_PROJECTOR_ID, + mode: LOGS_SEARCH_PROJECTOR_CHECKPOINT_MODE, + }, + orderBy: { windowEnd: "desc" }, + select: { windowEnd: true }, + }); + + return checkpoint?.windowEnd ?? initialWatermark; + } + + async appendFinalizedCheckpoint( + window: LogsSearchProjectorWindow, + result: LogsSearchProjectorProjectionResult + ): Promise { + const inserted = await this.database.logsSearchProjectorCheckpoint.createMany({ + data: [ + { + projectorId: LOGS_SEARCH_PROJECTOR_ID, + mode: LOGS_SEARCH_PROJECTOR_CHECKPOINT_MODE, + windowStart: window.start, + windowEnd: window.end, + queryId: result.queryId, + }, + ], + skipDuplicates: true, + }); + + return inserted.count === 1 ? "inserted" : "duplicate"; + } +} diff --git a/apps/webapp/app/services/logsSearchProjectorTelemetry.server.ts b/apps/webapp/app/services/logsSearchProjectorTelemetry.server.ts new file mode 100644 index 00000000000..49e30e729a2 --- /dev/null +++ b/apps/webapp/app/services/logsSearchProjectorTelemetry.server.ts @@ -0,0 +1,79 @@ +import { getMeter } from "@internal/tracing"; +import { singleton } from "~/utils/singleton"; + +export type LogsSearchProjectionMode = "preview" | "finalized"; +export type LogsSearchProjectionOutcome = "success" | "error"; + +const telemetry = singleton("logsSearchProjectorTelemetry", () => { + const meter = getMeter("logs-search-projector"); + const values: { + previewLagMs?: number; + finalizedLagMs?: number; + updatedAt?: number; + } = {}; + const isFresh = () => values.updatedAt && Date.now() - values.updatedAt < 150_000; + + meter + .createObservableGauge("logs_search.projector.preview_lag_ms", { + description: "Delay between the preview cutoff and its best-effort Redis watermark", + }) + .addCallback((result) => { + if (isFresh() && values.previewLagMs !== undefined) result.observe(values.previewLagMs); + }); + meter + .createObservableGauge("logs_search.projector.finalized_lag_ms", { + description: "Delay between the finalized cutoff and its durable checkpoint watermark", + }) + .addCallback((result) => { + if (isFresh() && values.finalizedLagMs !== undefined) result.observe(values.finalizedLagMs); + }); + return { + values, + windows: meter.createCounter("logs_search.projector.windows", { + description: "Logs search projection windows by mode and outcome", + }), + duration: meter.createHistogram("logs_search.projector.window_duration_ms", { + description: "Duration of one logs search projection window", + }), + sourceRows: meter.createHistogram("logs_search.projector.source_rows", { + description: "Source rows read for one logs search projection window", + }), + destinationRows: meter.createHistogram("logs_search.projector.destination_rows", { + description: "Rows written for one logs search projection window", + }), + leaseContention: meter.createCounter("logs_search.projector.lease_contention"), + checkpointConflicts: meter.createCounter("logs_search.projector.checkpoint_conflicts"), + previewSkipped: meter.createCounter("logs_search.projector.preview_skipped_windows"), + }; +}); + +export const logsSearchProjectorTelemetry = { + recordWindow( + mode: LogsSearchProjectionMode, + outcome: LogsSearchProjectionOutcome, + durationMs: number, + sourceRows = 0, + destinationRows = 0 + ) { + telemetry.windows.add(1, { mode, outcome }); + telemetry.duration.record(durationMs, { mode, outcome }); + telemetry.sourceRows.record(sourceRows, { mode }); + telemetry.destinationRows.record(destinationRows, { mode }); + }, + recordLeaseContention(mode: LogsSearchProjectionMode) { + telemetry.leaseContention.add(1, { mode }); + }, + recordCheckpointConflict() { + telemetry.checkpointConflicts.add(1); + }, + recordPreviewSkipped(count: number) { + telemetry.previewSkipped.add(count); + }, + updateState(values: { previewLagMs: number | null; finalizedLagMs: number }) { + if (values.previewLagMs !== null) { + telemetry.values.previewLagMs = Math.max(0, values.previewLagMs); + } + telemetry.values.finalizedLagMs = Math.max(0, values.finalizedLagMs); + telemetry.values.updatedAt = Date.now(); + }, +}; diff --git a/apps/webapp/app/utils/logSearch.test.ts b/apps/webapp/app/utils/logSearch.test.ts new file mode 100644 index 00000000000..4223244f8d5 --- /dev/null +++ b/apps/webapp/app/utils/logSearch.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { + escapeClickHouseLike, + hasMinimumLogsSearchLength, + logsSearchExpansionPeriod, + normalizeLogsSearchTerm, + prepareLogsSearchPage, +} from "./logSearch"; + +describe("log search normalization", () => { + it("normalizes punctuation while preserving unicode, paths, and ids", () => { + expect( + normalizeLogsSearchTerm("TypeError: Zahlungsübersicht failed, retrying (/api/orders/42)") + ).toBe("typeerror:zahlungsübersicht failed retrying /api/orders/42"); + expect(normalizeLogsSearchTerm('"status_code": 500')).toBe("status_code:500"); + expect(normalizeLogsSearchTerm("status_code:500")).toBe("status_code:500"); + }); + + it("uses the same locale-independent casing as ClickHouse", () => { + expect(normalizeLogsSearchTerm("I İ ı İSTANBUL ΟΣ")).toBe("i i ı i stanbul ος"); + }); + + it("escapes LIKE wildcards without escaping path separators", () => { + expect(escapeClickHouseLike("/api/a_b/100%")).toBe("/api/a\\_b/100\\%"); + }); + + it("requires at least three unicode characters after trimming", () => { + expect(hasMinimumLogsSearchLength("ab")).toBe(false); + expect(hasMinimumLogsSearchLength(" ab ")).toBe(false); + expect(hasMinimumLogsSearchLength("abc")).toBe(true); + expect(hasMinimumLogsSearchLength("日本語")).toBe(true); + }); + + it("only offers a strictly wider retained search range", () => { + const to = new Date("2026-08-14T12:00:00.000Z"); + + expect(logsSearchExpansionPeriod(new Date("2026-08-14T11:00:00.000Z"), to, 1)).toBe("1d"); + expect(logsSearchExpansionPeriod(new Date("2026-08-13T12:00:00.000Z"), to, 1)).toBeUndefined(); + expect(logsSearchExpansionPeriod(new Date("2026-08-13T12:00:00.000Z"), to, 7)).toBe("7d"); + }); + + it("removes projector retry copies after bounded overfetch", () => { + const row = (fingerprint: string) => ({ + projection_fingerprint_string: fingerprint, + trace_id: `trace_${fingerprint}`, + span_id: `span_${fingerprint}`, + run_id: `run_${fingerprint}`, + start_time: "2026-08-14 12:00:00.000000000", + }); + const page = prepareLogsSearchPage([row("a"), row("a"), row("b"), row("c"), row("d")], 2, 5); + + expect(page.rows.map((item) => item.projection_fingerprint_string)).toEqual(["a", "b"]); + expect(page.hasMore).toBe(true); + }); + + it("keeps pagination open when retries fill the overfetch bound", () => { + const duplicate = { + projection_fingerprint_string: "same", + trace_id: "trace", + span_id: "span", + run_id: "run", + start_time: "2026-08-14 12:00:00.000000000", + }; + + expect(prepareLogsSearchPage([duplicate, duplicate, duplicate, duplicate], 2, 4)).toEqual({ + rows: [duplicate], + hasMore: true, + }); + }); +}); diff --git a/apps/webapp/app/utils/logSearch.ts b/apps/webapp/app/utils/logSearch.ts new file mode 100644 index 00000000000..f06457b28da --- /dev/null +++ b/apps/webapp/app/utils/logSearch.ts @@ -0,0 +1,66 @@ +export const MIN_LOGS_SEARCH_LENGTH = 3; +export const LOGS_SEARCH_RETRY_OVERFETCH_FACTOR = 4; +const DAY_MS = 24 * 60 * 60 * 1000; +const RANGE_COMPARISON_TOLERANCE_MS = 1000; + +export function logsSearchExpansionPeriod( + from: Date | undefined, + to: Date, + retentionLimitDays: number | undefined +): string | undefined { + if (!from) return undefined; + + const candidateDays = Math.min(retentionLimitDays ?? 7, 7); + const currentRangeMs = Math.max(0, to.getTime() - from.getTime()); + if (candidateDays * DAY_MS <= currentRangeMs + RANGE_COMPARISON_TOLERANCE_MS) { + return undefined; + } + + return `${candidateDays}d`; +} + +type ProjectedLogIdentity = { + projection_fingerprint_string?: string; + trace_id: string; + span_id: string; + run_id: string; + start_time: string; +}; + +export function prepareLogsSearchPage( + rows: T[], + pageSize: number, + queryLimit: number +): { rows: T[]; hasMore: boolean } { + const seen = new Set(); + const uniqueRows = rows.filter((row) => { + const identity = + row.projection_fingerprint_string ?? + JSON.stringify([row.trace_id, row.span_id, row.run_id, row.start_time]); + if (seen.has(identity)) return false; + seen.add(identity); + return true; + }); + + return { + rows: uniqueRows.slice(0, pageSize), + hasMore: uniqueRows.length > pageSize || rows.length === queryLimit, + }; +} + +export function hasMinimumLogsSearchLength(value: string): boolean { + return [...value.trim()].length >= MIN_LOGS_SEARCH_LENGTH; +} + +export function escapeClickHouseLike(value: string): string { + return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_"); +} + +// Must match the scheduled ClickHouse projector normalization. +export function normalizeLogsSearchTerm(value: string): string { + return value + .toLowerCase() + .replace(/[^\p{L}\p{N}_./:@+-]+/gu, " ") + .replace(/\s*:\s*/g, ":") + .trim(); +} diff --git a/apps/webapp/app/v3/logsSearchProjectorWorker.server.ts b/apps/webapp/app/v3/logsSearchProjectorWorker.server.ts new file mode 100644 index 00000000000..3969a25752c --- /dev/null +++ b/apps/webapp/app/v3/logsSearchProjectorWorker.server.ts @@ -0,0 +1,67 @@ +import { Logger } from "@trigger.dev/core/logger"; +import { CronSchema, Worker as RedisWorker } from "@trigger.dev/redis-worker"; +import { env } from "~/env.server"; +import { logger } from "~/services/logger.server"; +import { getLogsSearchProjector } from "~/services/logsSearchProjectorInstance.server"; +import { singleton } from "~/utils/singleton"; + +function initializeWorker() { + const worker = new RedisWorker({ + name: "logs-search-projector-worker", + redisOptions: { + keyPrefix: "logs-search-projector:worker:", + host: env.COMMON_WORKER_REDIS_HOST, + port: env.COMMON_WORKER_REDIS_PORT, + username: env.COMMON_WORKER_REDIS_USERNAME, + password: env.COMMON_WORKER_REDIS_PASSWORD, + enableAutoPipelining: true, + ...(env.COMMON_WORKER_REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }), + }, + catalog: { + "logsSearch.projectV2": { + schema: CronSchema, + cron: env.LOGS_SEARCH_PROJECTOR_PREVIEW_ENABLED ? "*/5 * * * * *" : "* * * * *", + jitterInMs: env.LOGS_SEARCH_PROJECTOR_PREVIEW_ENABLED ? 1_000 : 5_000, + visibilityTimeoutMs: + (env.LOGS_SEARCH_PROJECTOR_MAX_WINDOWS_PER_TICK + 1) * + (env.LOGS_SEARCH_PROJECTOR_MAX_EXECUTION_TIME_SECONDS + 30) * + 1000 + + 60_000, + retry: { maxAttempts: 1 }, + }, + }, + concurrency: { workers: 1, tasksPerWorker: 1, limit: 1 }, + pollIntervalMs: env.COMMON_WORKER_POLL_INTERVAL, + immediatePollIntervalMs: env.COMMON_WORKER_IMMEDIATE_POLL_INTERVAL, + shutdownTimeoutMs: env.COMMON_WORKER_SHUTDOWN_TIMEOUT_MS, + logger: new Logger("LogsSearchProjectorWorker", env.COMMON_WORKER_LOG_LEVEL), + jobs: { + "logsSearch.projectV2": async () => { + await getLogsSearchProjector().processTick(); + }, + }, + }); + + return worker; +} + +const logsSearchProjectorWorker = singleton("logsSearchProjectorWorker", initializeWorker); + +declare global { + // eslint-disable-next-line no-var + var __logsSearchProjectorWorkerStarted__: boolean | undefined; +} + +export function initLogsSearchProjectorWorker(): void { + if ( + !env.LOGS_SEARCH_PROJECTOR_ENABLED || + env.COMMON_WORKER_ENABLED !== "true" || + global.__logsSearchProjectorWorkerStarted__ + ) { + return; + } + + logger.info("Starting logs search projector worker"); + logsSearchProjectorWorker.start(); + global.__logsSearchProjectorWorkerStarted__ = true; +} diff --git a/apps/webapp/test/logsSearchProjector.test.ts b/apps/webapp/test/logsSearchProjector.test.ts new file mode 100644 index 00000000000..8a31c11d514 --- /dev/null +++ b/apps/webapp/test/logsSearchProjector.test.ts @@ -0,0 +1,169 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + finalizedSafeCutoff, + LogsSearchProjector, + previewSafeCutoff, + selectFinalizedWindow, + selectPreviewWindow, + type LogsSearchProjectorRedisStore, + type LogsSearchProjectorStateStore, +} from "~/services/logsSearchProjector.server"; + +const telemetry = vi.hoisted(() => ({ + recordWindow: vi.fn(), + recordLeaseContention: vi.fn(), + recordCheckpointConflict: vi.fn(), + recordPreviewSkipped: vi.fn(), + updateState: vi.fn(), +})); + +vi.mock("~/services/logsSearchProjectorTelemetry.server", () => ({ + logsSearchProjectorTelemetry: telemetry, +})); + +const at = (value: string) => new Date(value); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("logs search projector window selection", () => { + it("floors preview work to a closed five-second boundary", () => { + expect(previewSafeCutoff(at("2026-08-14T12:10:09.999Z")).toISOString()).toBe( + "2026-08-14T12:10:05.000Z" + ); + }); + + it("floors finalized work to a closed minute after the safety delay", () => { + expect(finalizedSafeCutoff(at("2026-08-14T12:10:59.999Z")).toISOString()).toBe( + "2026-08-14T12:08:00.000Z" + ); + }); + + it("selects finalized windows sequentially", () => { + expect( + selectFinalizedWindow(at("2026-08-14T12:05:00.000Z"), at("2026-08-14T12:08:00.000Z")) + ).toEqual({ + mode: "finalized", + start: at("2026-08-14T12:05:00.000Z"), + end: at("2026-08-14T12:06:00.000Z"), + }); + }); + + it("selects the next preview window when caught up", () => { + expect( + selectPreviewWindow(at("2026-08-14T12:10:00.000Z"), at("2026-08-14T12:10:05.000Z")) + ).toEqual({ + window: { + mode: "preview", + start: at("2026-08-14T12:10:00.000Z"), + end: at("2026-08-14T12:10:05.000Z"), + }, + skippedWindows: 0, + }); + }); + + it("skips stale preview backlog and selects only the newest eligible window", () => { + expect( + selectPreviewWindow(at("2026-08-14T12:09:40.000Z"), at("2026-08-14T12:10:05.000Z")) + ).toEqual({ + window: { + mode: "preview", + start: at("2026-08-14T12:10:00.000Z"), + end: at("2026-08-14T12:10:05.000Z"), + }, + skippedWindows: 4, + }); + }); + + it("selects no work when each watermark reaches its cutoff", () => { + const finalized = at("2026-08-14T12:08:00.000Z"); + const preview = at("2026-08-14T12:10:05.000Z"); + expect(selectFinalizedWindow(finalized, finalized)).toBeNull(); + expect(selectPreviewWindow(preview, preview)).toEqual({ + window: null, + skippedWindows: 0, + }); + }); +}); + +describe("logs search projector execution", () => { + it("does no work when the projector is disabled", async () => { + const initialize = vi.fn(async () => at("2026-08-14T12:00:00.000Z")); + const projectWindow = vi.fn(); + const projector = new LogsSearchProjector( + { + enabled: false, + previewEnabled: true, + maxFinalizedWindowsPerTick: 1, + leaseDurationMs: 60_000, + }, + { + initialize, + findInitialWatermark: vi.fn(async () => null), + getFinalizedWatermark: vi.fn(async (watermark) => watermark), + appendFinalizedCheckpoint: vi.fn(), + }, + { + acquireLease: vi.fn(async () => true), + releaseLease: vi.fn(), + readLeaseStatus: vi.fn(async () => null), + initializePreviewWatermark: vi.fn(async (watermark) => watermark), + getPreviewWatermark: vi.fn(async () => null), + advancePreviewWatermark: vi.fn(async () => true), + }, + projectWindow, + () => at("2026-08-14T12:10:59.999Z") + ); + + await expect(projector.processTick()).resolves.toEqual({ finalized: 0, preview: false }); + expect(initialize).not.toHaveBeenCalled(); + expect(projectWindow).not.toHaveBeenCalled(); + }); + + it("refreshes lag after a finalized projection failure", async () => { + const now = at("2026-08-14T12:10:59.999Z"); + const watermark = at("2026-08-14T12:05:00.000Z"); + const stateStore = { + initialize: vi.fn(async () => watermark), + findInitialWatermark: vi.fn(async () => watermark), + getFinalizedWatermark: vi.fn(async () => watermark), + appendFinalizedCheckpoint: vi.fn(), + } satisfies LogsSearchProjectorStateStore; + const redisStore = { + acquireLease: vi.fn(async () => true), + releaseLease: vi.fn(), + readLeaseStatus: vi.fn(async () => null), + initializePreviewWatermark: vi.fn(async () => watermark), + getPreviewWatermark: vi.fn(async () => null), + advancePreviewWatermark: vi.fn(async () => true), + } satisfies LogsSearchProjectorRedisStore; + const projectionError = new Error("projection failed"); + const projector = new LogsSearchProjector( + { + enabled: true, + previewEnabled: true, + maxFinalizedWindowsPerTick: 1, + leaseDurationMs: 60_000, + }, + stateStore, + redisStore, + vi.fn(async () => { + throw projectionError; + }), + () => now, + { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + } + ); + + await expect(projector.processTick()).rejects.toBe(projectionError); + expect(telemetry.updateState).toHaveBeenCalledWith({ + previewLagMs: null, + finalizedLagMs: 180_000, + }); + }); +}); diff --git a/apps/webapp/test/logsSearchProjectorRedisStore.test.ts b/apps/webapp/test/logsSearchProjectorRedisStore.test.ts new file mode 100644 index 00000000000..e2c6ebd177e --- /dev/null +++ b/apps/webapp/test/logsSearchProjectorRedisStore.test.ts @@ -0,0 +1,42 @@ +import { redisTest } from "@internal/testcontainers"; +import Redis from "ioredis"; +import { expect } from "vitest"; +import { RedisLogsSearchProjectorStore } from "~/services/logsSearchProjectorRedisStore.server"; + +const at = (value: string) => new Date(value); + +redisTest("coordinates projection leases with token-safe release", async ({ redisOptions }) => { + const redis = new Redis(redisOptions); + const store = new RedisLogsSearchProjectorStore(redis); + + expect(await store.acquireLease("preview-owner", "preview", 60_000)).toBe(true); + expect(await store.acquireLease("finalized-owner", "finalized", 60_000)).toBe(false); + await expect(store.readLeaseStatus()).resolves.toMatchObject({ mode: "preview" }); + + await store.releaseLease("wrong-owner", "preview"); + expect(await store.acquireLease("finalized-owner", "finalized", 60_000)).toBe(false); + + await store.releaseLease("preview-owner", "preview"); + expect(await store.acquireLease("finalized-owner", "finalized", 60_000)).toBe(true); + + await redis.quit(); +}); + +redisTest( + "initializes and advances the preview watermark monotonically", + async ({ redisOptions }) => { + const redis = new Redis(redisOptions); + const store = new RedisLogsSearchProjectorStore(redis); + const initial = at("2026-08-14T12:00:00.000Z"); + const next = at("2026-08-14T12:00:05.000Z"); + const laterInitialization = at("2026-08-14T12:01:00.000Z"); + + await expect(store.initializePreviewWatermark(initial)).resolves.toEqual(initial); + await expect(store.initializePreviewWatermark(laterInitialization)).resolves.toEqual(initial); + await expect(store.advancePreviewWatermark(next)).resolves.toBe(true); + await expect(store.advancePreviewWatermark(initial)).resolves.toBe(false); + await expect(store.getPreviewWatermark()).resolves.toEqual(next); + + await redis.quit(); + } +); diff --git a/apps/webapp/test/logsSearchProjectorStateStore.test.ts b/apps/webapp/test/logsSearchProjectorStateStore.test.ts new file mode 100644 index 00000000000..54c0f94c901 --- /dev/null +++ b/apps/webapp/test/logsSearchProjectorStateStore.test.ts @@ -0,0 +1,67 @@ +import { postgresTest } from "@internal/testcontainers"; +import { expect } from "vitest"; +import { + LOGS_SEARCH_PROJECTOR_CHECKPOINT_MODE, + LOGS_SEARCH_PROJECTOR_ID, + LOGS_SEARCH_PROJECTOR_INITIAL_MODE, + type LogsSearchProjectorWindow, +} from "~/services/logsSearchProjector.server"; +import { PrismaLogsSearchProjectorStateStore } from "~/services/logsSearchProjectorStateStore.server"; + +const at = (value: string) => new Date(value); + +postgresTest( + "persists append-only initialization and finalized checkpoints", + { timeout: 20_000 }, + async ({ prisma }) => { + const store = new PrismaLogsSearchProjectorStateStore(prisma); + const initial = at("2026-08-14T12:00:00.000Z"); + const laterInitialization = at("2026-08-14T12:05:00.000Z"); + const window: LogsSearchProjectorWindow = { + mode: "finalized", + start: initial, + end: at("2026-08-14T12:01:00.000Z"), + }; + + await expect(store.findInitialWatermark()).resolves.toBeNull(); + await expect(store.initialize(initial)).resolves.toEqual(initial); + await expect(store.initialize(laterInitialization)).resolves.toEqual(initial); + await expect(store.getFinalizedWatermark(initial)).resolves.toEqual(initial); + + await expect( + store.appendFinalizedCheckpoint(window, { + queryId: "query-1", + readRows: 10, + writtenRows: 8, + }) + ).resolves.toBe("inserted"); + await expect( + store.appendFinalizedCheckpoint(window, { + queryId: "query-retry", + readRows: 10, + writtenRows: 8, + }) + ).resolves.toBe("duplicate"); + await expect(store.getFinalizedWatermark(initial)).resolves.toEqual(window.end); + + const checkpoints = await prisma.logsSearchProjectorCheckpoint.findMany({ + where: { projectorId: LOGS_SEARCH_PROJECTOR_ID }, + orderBy: { windowEnd: "asc" }, + }); + expect(checkpoints).toHaveLength(2); + expect(checkpoints).toEqual([ + expect.objectContaining({ + mode: LOGS_SEARCH_PROJECTOR_INITIAL_MODE, + windowStart: initial, + windowEnd: initial, + queryId: null, + }), + expect.objectContaining({ + mode: LOGS_SEARCH_PROJECTOR_CHECKPOINT_MODE, + windowStart: window.start, + windowEnd: window.end, + queryId: "query-1", + }), + ]); + } +); diff --git a/apps/webapp/test/runGetRoutes.replicaLag.guard.test.ts b/apps/webapp/test/runGetRoutes.replicaLag.guard.test.ts index f6597980f39..146aba99e14 100644 --- a/apps/webapp/test/runGetRoutes.replicaLag.guard.test.ts +++ b/apps/webapp/test/runGetRoutes.replicaLag.guard.test.ts @@ -267,7 +267,11 @@ let seq = 0; async function seedTenant(prisma: PrismaClient, suffix: string) { const organization = await prisma.organization.create({ - data: { title: `Org ${suffix}`, slug: `org-${suffix}` }, + data: { + title: `Org ${suffix}`, + slug: `org-${suffix}`, + featureFlags: { hasLogsPageAccess: true }, + }, }); const project = await prisma.project.create({ data: { diff --git a/internal-packages/clickhouse/schema/040_create_task_events_search_v2.sql b/internal-packages/clickhouse/schema/040_create_task_events_search_v2.sql new file mode 100644 index 00000000000..6f526a8ef78 --- /dev/null +++ b/internal-packages/clickhouse/schema/040_create_task_events_search_v2.sql @@ -0,0 +1,55 @@ +-- +goose Up +-- Search v2 stores bounded normalized text outside the task_events_v2 insert path. +-- The source index (idx_inserted_at_projector) is added in an earlier migration. +CREATE TABLE IF NOT EXISTS trigger_dev.task_events_search_v2 +( + environment_id String, + organization_id String, + project_id String, + triggered_timestamp DateTime64(9) CODEC(Delta(8), ZSTD(1)), + trace_id String CODEC(ZSTD(1)), + span_id String CODEC(ZSTD(1)), + run_id String CODEC(ZSTD(1)), + task_identifier String CODEC(ZSTD(1)), + start_time DateTime64(9) CODEC(Delta(8), ZSTD(1)), + inserted_at DateTime64(3), + message String CODEC(ZSTD(1)), + error_message String CODEC(ZSTD(1)), + search_text String CODEC(ZSTD(1)), + kind LowCardinality(String) CODEC(ZSTD(1)), + status LowCardinality(String) CODEC(ZSTD(1)), + duration UInt64 CODEC(ZSTD(1)), + parent_span_id String CODEC(ZSTD(1)), + projection_fingerprint UInt128 DEFAULT reinterpretAsUInt128( + sipHash128( + trace_id, + span_id, + run_id, + start_time, + kind, + status, + duration, + toValidUTF8(substring(message, 1, 2045)), + toValidUTF8(substring(error_message, 1, 2045)) + ) + ), + + INDEX idx_run_id run_id TYPE bloom_filter(0.001) GRANULARITY 1, + INDEX idx_search_text search_text + TYPE text(tokenizer = 'ngrams', preprocessor = lowerUTF8(search_text)) +) +ENGINE = ReplacingMergeTree +PARTITION BY toDate(inserted_at) +ORDER BY ( + organization_id, + environment_id, + triggered_timestamp, + trace_id, + span_id, + projection_fingerprint +) +TTL toDateTime(triggered_timestamp) + INTERVAL 90 DAY +SETTINGS ttl_only_drop_parts = 1; + +-- +goose Down +DROP TABLE IF EXISTS trigger_dev.task_events_search_v2; diff --git a/internal-packages/clickhouse/src/client/client.ts b/internal-packages/clickhouse/src/client/client.ts index d6703c863ce..a61598360b3 100644 --- a/internal-packages/clickhouse/src/client/client.ts +++ b/internal-packages/clickhouse/src/client/client.ts @@ -13,6 +13,7 @@ import { flattenAttributes, tryCatch, type Result } from "@trigger.dev/core/v3"; import { z } from "zod"; import { InsertError, QueryError } from "./errors.js"; import type { + ClickhouseCommandFunction, ClickhouseInsertFunction, ClickhouseQueryBuilderFastFunction, ClickhouseQueryBuilderFunction, @@ -43,6 +44,7 @@ export type ClickhouseConfig = { clickhouseSettings?: ClickHouseSettings; logger?: Logger; maxOpenConnections?: number; + requestTimeoutMs?: number; logLevel?: LogLevel; compression?: { request?: boolean; @@ -66,6 +68,7 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter { http_agent: config.httpAgent, compression: config.compression, max_open_connections: config.maxOpenConnections, + request_timeout: config.requestTimeoutMs, clickhouse_settings: { ...config.clickhouseSettings, output_format_json_quote_64bit_integers: 0, @@ -672,6 +675,88 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter { }); } + public command>(req: { + name: string; + query: string; + params?: TSchema; + settings?: ClickHouseSettings; + }): ClickhouseCommandFunction> { + return async (params, options) => { + const queryId = randomUUID(); + + return await startSpan(this.tracer, "command", async (span) => { + span.setAttributes({ + "clickhouse.clientName": this.name, + "clickhouse.operationName": req.name, + "clickhouse.queryId": queryId, + ...flattenAttributes(req.settings, "clickhouse.settings"), + ...flattenAttributes(options?.attributes), + }); + + const validParams = req.params?.safeParse(params); + if (validParams?.error) { + recordSpanError(span, validParams.error); + return [ + new QueryError(`Bad params: ${generateErrorMessage(validParams.error.issues)}`, { + query: req.query, + }), + null, + ]; + } + + this.logger.debug("Running clickhouse command", { + clientName: this.name, + name: req.name, + query: req.query.replace(/\s+/g, " "), + settings: req.settings, + attributes: options?.attributes, + queryId, + }); + + const [clickhouseError, result] = await tryCatch( + this.client.command({ + query: req.query, + query_params: validParams?.data, + query_id: queryId, + ...options?.params, + clickhouse_settings: { + ...req.settings, + ...options?.params?.clickhouse_settings, + }, + }) + ); + + if (clickhouseError) { + this.logger.error("Error running clickhouse command", { + name: req.name, + error: clickhouseError, + query: req.query, + queryId, + }); + recordClickhouseError(span, clickhouseError); + return [ + new QueryError(`Unable to run clickhouse command: ${clickhouseError.message}`, { + query: req.query, + }), + null, + ]; + } + + span.setAttributes({ + "clickhouse.query_id": result.query_id, + "clickhouse.summary.read_rows": result.summary?.read_rows, + "clickhouse.summary.read_bytes": result.summary?.read_bytes, + "clickhouse.summary.written_rows": result.summary?.written_rows, + "clickhouse.summary.written_bytes": result.summary?.written_bytes, + "clickhouse.summary.elapsed_ns": result.summary?.elapsed_ns, + ...flattenAttributes(result.response_headers, "clickhouse.response_headers"), + }); + + return [null, result]; + }); + }; + } + public insert>(req: { name: string; table: string; diff --git a/internal-packages/clickhouse/src/client/noop.ts b/internal-packages/clickhouse/src/client/noop.ts index 00adef82c8a..2e91003a5e6 100644 --- a/internal-packages/clickhouse/src/client/noop.ts +++ b/internal-packages/clickhouse/src/client/noop.ts @@ -8,7 +8,7 @@ import type { QueryResultWithStats, } from "./types.js"; import type { z } from "zod"; -import type { ClickHouseSettings, InsertResult } from "@clickhouse/client"; +import type { ClickHouseSettings, CommandResult, InsertResult } from "@clickhouse/client"; import { ClickhouseQueryBuilder, ClickhouseQueryFastBuilder } from "./queryBuilder.js"; export class NoopClient implements ClickhouseReader, ClickhouseWriter { @@ -109,6 +109,41 @@ export class NoopClient implements ClickhouseReader, ClickhouseWriter { }; } + public command>(req: { + name: string; + query: string; + params?: TSchema; + settings?: ClickHouseSettings; + }): (params: z.input) => Promise> { + return async (params) => { + const validParams = req.params?.safeParse(params); + if (validParams?.error) { + return [ + new QueryError(`Bad params: ${validParams.error.message}`, { query: req.query }), + null, + ]; + } + + return [ + null, + { + query_id: "noop", + summary: { + read_rows: "0", + read_bytes: "0", + written_rows: "0", + written_bytes: "0", + total_rows_to_read: "0", + result_rows: "0", + result_bytes: "0", + elapsed_ns: "0", + }, + response_headers: {}, + }, + ]; + }; + } + public insert>(req: { name: string; table: string; diff --git a/internal-packages/clickhouse/src/client/types.ts b/internal-packages/clickhouse/src/client/types.ts index 4bfa6dc4662..6cfbe35fd48 100644 --- a/internal-packages/clickhouse/src/client/types.ts +++ b/internal-packages/clickhouse/src/client/types.ts @@ -4,6 +4,7 @@ import type { InsertError, QueryError } from "./errors.js"; import { type ClickHouseSettings, type BaseQueryParams, + type CommandResult, type InsertResult, } from "@clickhouse/client"; import type { ClickhouseQueryBuilder, ClickhouseQueryFastBuilder } from "./queryBuilder.js"; @@ -237,6 +238,14 @@ export interface ClickhouseReader { close(): Promise; } +export type ClickhouseCommandFunction = ( + params: TInput, + options?: { + attributes?: Record; + params?: BaseQueryParams; + } +) => Promise>; + export type ClickhouseInsertFunction = ( events: TInput | TInput[], options?: { @@ -246,6 +255,13 @@ export type ClickhouseInsertFunction = ( ) => Promise>; export interface ClickhouseWriter { + command>(req: { + name: string; + query: string; + params?: TSchema; + settings?: ClickHouseSettings; + }): ClickhouseCommandFunction>; + insert>(req: { name: string; table: string; diff --git a/internal-packages/clickhouse/src/index.ts b/internal-packages/clickhouse/src/index.ts index bfdaededcca..407c33135cc 100644 --- a/internal-packages/clickhouse/src/index.ts +++ b/internal-packages/clickhouse/src/index.ts @@ -31,6 +31,7 @@ import { getLogDetailQueryBuilderV2, getLogsSearchListQueryBuilder, } from "./taskEvents.js"; +import { projectTaskEventsSearchV2Window } from "./taskEventsSearchProjector.js"; import { insertMetrics } from "./metrics.js"; import { insertLlmMetrics } from "./llmMetrics.js"; import { @@ -79,6 +80,7 @@ import type { Agent as HttpsAgent } from "https"; export type * from "./taskRuns.js"; export type * from "./taskEvents.js"; +export * from "./taskEventsSearchProjector.js"; export type * from "./metrics.js"; export type * from "./llmMetrics.js"; export type * from "./queueMetrics.js"; @@ -137,6 +139,7 @@ export type ClickhouseCommonConfig = { response?: boolean; }; maxOpenConnections?: number; + requestTimeoutMs?: number; }; export type ClickHouseConfig = @@ -179,6 +182,7 @@ export class ClickHouse { keepAlive: config.keepAlive, httpAgent: config.httpAgent, maxOpenConnections: config.maxOpenConnections, + requestTimeoutMs: config.requestTimeoutMs, compression: config.compression, }); this.reader = client; @@ -195,6 +199,7 @@ export class ClickHouse { keepAlive: config.keepAlive, httpAgent: config.httpAgent, maxOpenConnections: config.maxOpenConnections, + requestTimeoutMs: config.requestTimeoutMs, compression: config.compression, }); this.writer = new ClickhouseClient({ @@ -206,6 +211,7 @@ export class ClickHouse { keepAlive: config.keepAlive, httpAgent: config.httpAgent, maxOpenConnections: config.maxOpenConnections, + requestTimeoutMs: config.requestTimeoutMs, compression: config.compression, }); @@ -336,6 +342,7 @@ export class ClickHouse { get taskEventsSearch() { return { logsListQueryBuilder: getLogsSearchListQueryBuilder(this.reader), + projectV2Window: projectTaskEventsSearchV2Window(this.writer), }; } diff --git a/internal-packages/clickhouse/src/taskEvents.ts b/internal-packages/clickhouse/src/taskEvents.ts index a1f001897b4..c79a048d481 100644 --- a/internal-packages/clickhouse/src/taskEvents.ts +++ b/internal-packages/clickhouse/src/taskEvents.ts @@ -280,7 +280,7 @@ export function getTraceEventsForExportQueryBuilderV2( } // ============================================================================ -// Search Table Query Builders (for logs page, using task_events_search_v1) +// Search Table Query Builders (for logs page, using task_events_search_v2) // ============================================================================ export const LogsSearchListResult = z.object({ @@ -294,19 +294,20 @@ export const LogsSearchListResult = z.object({ span_id: z.string(), parent_span_id: z.string(), message: z.string(), + error_message: z.string(), kind: z.string(), status: z.string(), duration: z.number().or(z.string()), - attributes_text: z.string(), triggered_timestamp: z.string(), + projection_fingerprint_string: z.string().optional(), }); export type LogsSearchListResult = z.output; export function getLogsSearchListQueryBuilder(ch: ClickhouseReader) { - return ch.queryBuilderFast({ - name: "getLogsSearchList", - table: "trigger_dev.task_events_search_v1", + const createBuilder = ch.queryBuilderFast({ + name: "getLogsSearchListV2", + table: "trigger_dev.task_events_search_v2", columns: [ "environment_id", "organization_id", @@ -318,16 +319,22 @@ export function getLogsSearchListQueryBuilder(ch: ClickhouseReader) { "span_id", "parent_span_id", { name: "message", expression: "LEFT(message, 512)" }, + "error_message", "kind", "status", "duration", - "attributes_text", "triggered_timestamp", + { + name: "projection_fingerprint_string", + expression: "toString(projection_fingerprint)", + }, ], settings: { use_query_condition_cache: 1, }, }); + + return createBuilder; } // Single log detail query builder (for side panel) diff --git a/internal-packages/clickhouse/src/taskEventsSearch.test.ts b/internal-packages/clickhouse/src/taskEventsSearch.test.ts new file mode 100644 index 00000000000..1832832f2ac --- /dev/null +++ b/internal-packages/clickhouse/src/taskEventsSearch.test.ts @@ -0,0 +1,336 @@ +import { clickhouseTest } from "@internal/testcontainers"; +import { randomUUID } from "node:crypto"; +import { z } from "zod"; +import { ClickHouse } from "./index.js"; + +const ORG = "org_logs_search"; +const PROJECT = "project_logs_search"; +const ENVIRONMENT = "env_logs_search"; +const LIMITS = { + maxExecutionTimeSeconds: 30, + maxRowsToRead: 1_000_000, + maxMemoryUsage: 500_000_000, + maxThreads: 1, +}; + +function clickhouseDate(value: Date) { + return value.toISOString().replace("T", " ").replace("Z", ""); +} + +function event(now: Date, overrides: Record = {}) { + const start = clickhouseDate(now); + return { + environment_id: ENVIRONMENT, + organization_id: ORG, + project_id: PROJECT, + task_identifier: "search-task", + run_id: "run_logs_search", + start_time: start, + duration: "1000000", + trace_id: "trace_logs_search", + span_id: `span_${randomUUID()}`, + parent_span_id: "", + message: "TypeError: Zahlungsübersicht failed, retrying /api/orders/42", + kind: "LOG_ERROR", + status: "ERROR", + attributes: { + request_id: "req_123", + status_code: 500, + retryable: true, + error: { message: "Payment failed, retrying" }, + }, + metadata: "{}", + expires_at: clickhouseDate(new Date(now.getTime() + 90 * 24 * 60 * 60 * 1000)), + inserted_at: start, + ...overrides, + }; +} + +async function project(ch: ClickHouse, start: Date, end: Date) { + const [error, result] = await ch.taskEventsSearch.projectV2Window({ start, end }, LIMITS); + expect(error).toBeNull(); + expect(result?.query_id).toEqual(expect.any(String)); + return result!; +} + +function searchRows(ch: ClickHouse) { + const builder = ch.taskEventsSearch.logsListQueryBuilder(); + builder.where("organization_id = {organizationId: String}", { organizationId: ORG }); + builder.orderBy( + "triggered_timestamp DESC, trace_id DESC, span_id DESC, projection_fingerprint DESC" + ); + builder.limit(50); + return builder.execute(); +} + +describe("task events search v2", () => { + clickhouseTest( + "projects bounded normalized text outside the source insert path", + async ({ clickhouseContainer }) => { + const ch = new ClickHouse({ url: clickhouseContainer.getConnectionUrl(), name: "test" }); + const now = new Date("2026-08-14T10:10:30.000Z"); + const start = new Date(now.getTime() - 30_000); + const end = new Date(now.getTime() + 30_000); + const [insertError] = await ch.taskEventsV2.insert([event(now)]); + expect(insertError).toBeNull(); + + const [beforeError, beforeRows] = await searchRows(ch); + expect(beforeError).toBeNull(); + expect(beforeRows).toHaveLength(0); + + const schemaQuery = ch.reader.query({ + name: "read-search-v2-schema", + query: `SELECT name, type FROM system.data_skipping_indices + WHERE database = 'trigger_dev' AND table = 'task_events_v2' + AND name = 'idx_inserted_at_projector'`, + schema: z.object({ name: z.string(), type: z.string() }), + }); + const [schemaError, indexes] = await schemaQuery({}); + expect(schemaError).toBeNull(); + expect(indexes).toEqual([{ name: "idx_inserted_at_projector", type: "minmax" }]); + + const tableQuery = ch.reader.query({ + name: "read-search-v2-table-engine", + query: `SELECT name, engine, partition_key FROM system.tables + WHERE database = 'trigger_dev' + AND name IN ('task_events_search_mv_v2', 'task_events_search_v2') + ORDER BY name`, + schema: z.object({ name: z.string(), engine: z.string(), partition_key: z.string() }), + }); + const [tableError, tables] = await tableQuery({}); + expect(tableError).toBeNull(); + expect(tables).toEqual([ + { + name: "task_events_search_v2", + engine: "ReplacingMergeTree", + partition_key: "toDate(inserted_at)", + }, + ]); + + const firstProjection = await project(ch, start, end); + const retryProjection = await project(ch, start, end); + expect(Number(firstProjection.summary?.written_rows)).toBe(1); + expect(Number(retryProjection.summary?.written_rows)).toBe(1); + + const insertWithDefaultFingerprint = ch.writer.command({ + name: "copy-search-v2-row-with-default-fingerprint", + query: `INSERT INTO trigger_dev.task_events_search_v2 + (environment_id, organization_id, project_id, triggered_timestamp, trace_id, span_id, + run_id, task_identifier, start_time, inserted_at, message, error_message, search_text, + kind, status, duration, parent_span_id) + SELECT + environment_id, organization_id, project_id, triggered_timestamp, trace_id, span_id, + run_id, task_identifier, start_time, inserted_at, message, error_message, search_text, + kind, status, duration, parent_span_id + FROM trigger_dev.task_events_search_v2 + WHERE organization_id = {organizationId: String} + LIMIT 1`, + params: z.object({ organizationId: z.string() }), + }); + const [defaultInsertError] = await insertWithDefaultFingerprint({ organizationId: ORG }); + expect(defaultInsertError).toBeNull(); + + const [preMergeReadError, preMergeRows] = await searchRows(ch); + expect(preMergeReadError).toBeNull(); + expect([1, 2, 3]).toContain(preMergeRows?.length); + const rawQuery = ch.reader.query({ + name: "count-raw-search-v2-fixture", + query: `SELECT count() AS count FROM trigger_dev.task_events_search_v2 + WHERE organization_id = {organizationId: String}`, + params: z.object({ organizationId: z.string() }), + schema: z.object({ count: z.number() }), + }); + let [rawError, rawRows] = await rawQuery({ organizationId: ORG }); + expect(rawError).toBeNull(); + expect([1, 2, 3]).toContain(rawRows?.[0].count); + + const optimize = ch.writer.command({ + name: "merge-search-v2-retry-fixture", + query: "OPTIMIZE TABLE trigger_dev.task_events_search_v2 FINAL", + }); + const [optimizeError] = await optimize({}); + expect(optimizeError).toBeNull(); + [rawError, rawRows] = await rawQuery({ organizationId: ORG }); + expect(rawError).toBeNull(); + expect(rawRows?.[0].count).toBe(1); + const [readError, rows] = await searchRows(ch); + expect(readError).toBeNull(); + expect(rows).toHaveLength(1); + + expect(rows?.[0].message.toLowerCase()).toContain( + "typeerror: zahlungsübersicht failed, retrying /api/orders/42" + ); + expect(rows?.[0].error_message).toBe("Payment failed, retrying"); + + const searchDataQuery = ch.reader.query({ + name: "read-search-v2-indexed-data", + query: `SELECT search_text, error_message + FROM trigger_dev.task_events_search_v2 + WHERE organization_id = {organizationId: String} + LIMIT 1`, + params: z.object({ organizationId: z.string() }), + schema: z.object({ search_text: z.string(), error_message: z.string() }), + }); + const [searchDataError, searchData] = await searchDataQuery({ organizationId: ORG }); + expect(searchDataError).toBeNull(); + expect(searchData).toHaveLength(1); + expect(searchData?.[0].search_text).toContain( + "typeerror:zahlungsübersicht failed retrying /api/orders/42" + ); + expect(searchData?.[0].search_text).toContain("status_code:500"); + expect(searchData?.[0].search_text).toContain("retryable:true"); + + await ch.close(); + } + ); + + clickhouseTest( + "deduplicates preview and finalized copies without collapsing distinct span rows", + async ({ clickhouseContainer }) => { + const ch = new ClickHouse({ url: clickhouseContainer.getConnectionUrl(), name: "test" }); + const insertedAt = new Date("2026-08-14T10:10:30.000Z"); + const sharedIdentity = { + trace_id: "trace_shared", + span_id: "span_shared", + run_id: "run_shared", + start_time: clickhouseDate(insertedAt), + inserted_at: clickhouseDate(insertedAt), + }; + const [insertError] = await ch.taskEventsV2.insert([ + event(insertedAt, { + ...sharedIdentity, + message: "first message", + }), + event(insertedAt, { + ...sharedIdentity, + message: "second message", + }), + ]); + expect(insertError).toBeNull(); + + await project(ch, insertedAt, new Date(insertedAt.getTime() + 5_000)); + await project(ch, new Date("2026-08-14T10:10:00.000Z"), new Date("2026-08-14T10:11:00.000Z")); + + const optimize = ch.writer.command({ + name: "merge-search-v2-preview-finalized-fixture", + query: "OPTIMIZE TABLE trigger_dev.task_events_search_v2 FINAL", + }); + const [optimizeError] = await optimize({}); + expect(optimizeError).toBeNull(); + + const fingerprintQuery = ch.reader.query({ + name: "read-search-v2-shared-identity-fingerprints", + query: `SELECT + count() AS count, + uniqExact(projection_fingerprint) AS fingerprints, + uniqExact(triggered_timestamp) AS triggered_timestamps + FROM trigger_dev.task_events_search_v2 + WHERE organization_id = {organizationId: String}`, + params: z.object({ organizationId: z.string() }), + schema: z.object({ + count: z.number(), + fingerprints: z.number(), + triggered_timestamps: z.number(), + }), + }); + const [fingerprintError, counts] = await fingerprintQuery({ organizationId: ORG }); + expect(fingerprintError).toBeNull(); + expect(counts).toEqual([{ count: 2, fingerprints: 2, triggered_timestamps: 1 }]); + + await ch.close(); + } + ); + + clickhouseTest( + "uses half-open windows and deterministically clamps future timestamps", + async ({ clickhouseContainer }) => { + const ch = new ClickHouse({ url: clickhouseContainer.getConnectionUrl(), name: "test" }); + const boundary = new Date("2026-08-14T11:01:00.000Z"); + const first = new Date(boundary.getTime() - 60_000); + const second = boundary; + const end = new Date(boundary.getTime() + 60_000); + const splitUtf8Boundary = `${"x".repeat(2044)}€tail`; + const [insertError] = await ch.taskEventsV2.insert([ + event(first, { + span_id: "span_first", + duration: "18446744073709551615", + message: splitUtf8Boundary, + attributes: { + prefix: "kept-token", + payload: "x".repeat(100_000), + error: { message: splitUtf8Boundary }, + }, + }), + event(second, { span_id: "span_second" }), + ]); + expect(insertError).toBeNull(); + + await project(ch, first, boundary); + let [readError, rows] = await searchRows(ch); + expect(readError).toBeNull(); + expect(rows).toHaveLength(1); + const lengthQuery = ch.reader.query({ + name: "read-search-v2-length", + query: `SELECT + length(search_text) AS search_length, + length(error_message) AS error_message_length, + isValidUTF8(search_text) AS search_text_is_valid_utf8, + isValidUTF8(error_message) AS error_message_is_valid_utf8 + FROM trigger_dev.task_events_search_v2 + WHERE organization_id = {organizationId: String} + LIMIT 1`, + params: z.object({ organizationId: z.string() }), + schema: z.object({ + search_length: z.number(), + error_message_length: z.number(), + search_text_is_valid_utf8: z.number(), + error_message_is_valid_utf8: z.number(), + }), + }); + const [lengthError, lengths] = await lengthQuery({ organizationId: ORG }); + expect(lengthError).toBeNull(); + expect(lengths?.[0].search_length).toBeLessThanOrEqual(8192); + expect(lengths?.[0].error_message_length).toBeLessThanOrEqual(2048); + expect(lengths?.[0].search_text_is_valid_utf8).toBe(1); + expect(lengths?.[0].error_message_is_valid_utf8).toBe(1); + expect(rows?.[0].triggered_timestamp).toBeDefined(); + expect(new Date(`${rows?.[0].triggered_timestamp}Z`).getTime()).toBe( + first.getTime() + 5 * 60_000 + ); + + await project(ch, boundary, end); + [readError, rows] = await searchRows(ch); + expect(readError).toBeNull(); + expect(rows).toHaveLength(2); + + const cursor = rows?.[0]; + expect(cursor?.projection_fingerprint_string).toEqual(expect.any(String)); + const nextPageBuilder = ch.taskEventsSearch.logsListQueryBuilder(); + nextPageBuilder.where("organization_id = {organizationId: String}", { + organizationId: ORG, + }); + nextPageBuilder.where( + `(triggered_timestamp < {cursorTriggeredTimestamp: String} + OR (triggered_timestamp = {cursorTriggeredTimestamp: String} AND trace_id < {cursorTraceId: String}) + OR (triggered_timestamp = {cursorTriggeredTimestamp: String} AND trace_id = {cursorTraceId: String} AND span_id < {cursorSpanId: String}) + OR (triggered_timestamp = {cursorTriggeredTimestamp: String} AND trace_id = {cursorTraceId: String} AND span_id = {cursorSpanId: String} AND projection_fingerprint < {cursorProjectionFingerprint: UInt128}))`, + { + cursorTriggeredTimestamp: cursor!.triggered_timestamp, + cursorTraceId: cursor!.trace_id, + cursorSpanId: cursor!.span_id, + cursorProjectionFingerprint: cursor!.projection_fingerprint_string!, + } + ); + nextPageBuilder.orderBy( + "triggered_timestamp DESC, trace_id DESC, span_id DESC, projection_fingerprint DESC" + ); + nextPageBuilder.limit(50); + const [nextPageError, nextPage] = await nextPageBuilder.execute(); + expect(nextPageError).toBeNull(); + expect(nextPage).toHaveLength(1); + expect(nextPage?.[0].span_id).not.toBe(cursor?.span_id); + + await ch.close(); + } + ); +}); diff --git a/internal-packages/clickhouse/src/taskEventsSearchProjector.ts b/internal-packages/clickhouse/src/taskEventsSearchProjector.ts new file mode 100644 index 00000000000..4502063c673 --- /dev/null +++ b/internal-packages/clickhouse/src/taskEventsSearchProjector.ts @@ -0,0 +1,188 @@ +import type { ClickHouseSettings, CommandResult } from "@clickhouse/client"; +import type { Result } from "@trigger.dev/core/v3"; +import { z } from "zod"; +import type { QueryError } from "./client/errors.js"; +import type { ClickhouseWriter } from "./client/types.js"; + +export type TaskEventsSearchV2ProjectionWindow = { + start: Date; + end: Date; +}; + +export type TaskEventsSearchV2ProjectionLimits = { + maxExecutionTimeSeconds: number; + maxRowsToRead: number; + maxMemoryUsage: number; + maxThreads: number; +}; + +const ProjectionParams = z + .object({ + windowStart: z.string(), + windowEnd: z.string(), + }) + .refine(({ windowStart, windowEnd }) => windowStart < windowEnd, { + message: "windowStart must be before windowEnd", + }); + +const projectedColumns = ` + environment_id, + organization_id, + project_id, + triggered_timestamp, + trace_id, + span_id, + run_id, + task_identifier, + start_time, + inserted_at, + message, + error_message, + search_text, + kind, + status, + duration, + parent_span_id`; + +const boundedUtf8 = (expression: string) => `toValidUTF8(substring(${expression}, 1, 2045))`; + +const projectionFingerprint = (alias: string) => `reinterpretAsUInt128(sipHash128( + ${alias}.trace_id, + ${alias}.span_id, + ${alias}.run_id, + ${alias}.start_time, + ${alias}.kind, + ${alias}.status, + ${alias}.duration, + ${boundedUtf8(`${alias}.message`)}, + ${boundedUtf8(`${alias}.error_message`)} +))`; + +const projectionSql = ` +INSERT INTO trigger_dev.task_events_search_v2 +(${projectedColumns}, projection_fingerprint) +SELECT${projectedColumns}, + ${projectionFingerprint("candidate")} AS projection_fingerprint +FROM +( + SELECT + environment_id, + organization_id, + project_id, + fromUnixTimestamp64Nano( + toInt64( + least( + toInt128(toUnixTimestamp64Nano(start_time)) + toInt128(duration), + toInt128( + toUnixTimestamp64Nano(inserted_at + INTERVAL 5 MINUTE) + ) + ) + ) + ) AS triggered_timestamp, + trace_id, + span_id, + run_id, + task_identifier, + start_time, + inserted_at, + message, + ${boundedUtf8("JSONExtractString(attributes_text, 'error', 'message')")} AS error_message, + toValidUTF8( + substring( + replaceRegexpAll( + replaceRegexpAll( + lowerUTF8( + concat( + ${boundedUtf8("message")}, + ' ', + replaceAll( + toValidUTF8(substring(attributes_text, 1, 6140)), + '\\\\/', + '/' + ) + ) + ), + '[^\\\\p{L}\\\\p{N}_./:@+-]+', + ' ' + ), + '\\\\s*:\\\\s*', + ':' + ), + 1, + 8189 + ) + ) AS search_text, + kind, + status, + duration, + parent_span_id + FROM trigger_dev.task_events_v2 + WHERE + inserted_at >= {windowStart: DateTime64(3, 'UTC')} + AND inserted_at < {windowEnd: DateTime64(3, 'UTC')} + AND trace_id != '' + AND kind != 'DEBUG_EVENT' + AND status != 'PARTIAL' + AND NOT (kind = 'SPAN_EVENT' AND attributes_text = '{}') + AND kind != 'ANCESTOR_OVERRIDE' + AND message != 'trigger.dev/start' +) AS candidate +ORDER BY + organization_id, + environment_id, + triggered_timestamp, + trace_id, + span_id, + projection_fingerprint +`; + +export function projectTaskEventsSearchV2Window(writer: ClickhouseWriter) { + return async ( + window: TaskEventsSearchV2ProjectionWindow, + limits: TaskEventsSearchV2ProjectionLimits + ): Promise> => { + assertProjectionWindow(window); + const command = writer.command({ + name: "project-task-events-search-v2-window", + query: projectionSql, + params: ProjectionParams, + }); + const settings: ClickHouseSettings = { + async_insert: 0, + max_execution_time: limits.maxExecutionTimeSeconds, + max_rows_to_read: limits.maxRowsToRead.toString(), + max_memory_usage: limits.maxMemoryUsage.toString(), + max_threads: limits.maxThreads, + max_insert_threads: limits.maxThreads.toString(), + use_query_condition_cache: 0, + }; + + return command( + { + windowStart: toClickHouseDateTime64(window.start), + windowEnd: toClickHouseDateTime64(window.end), + }, + { + attributes: { + windowStart: window.start.toISOString(), + windowEnd: window.end.toISOString(), + }, + params: { clickhouse_settings: settings }, + } + ); + }; +} + +function assertProjectionWindow(window: TaskEventsSearchV2ProjectionWindow) { + if ( + !Number.isFinite(window.start.getTime()) || + !Number.isFinite(window.end.getTime()) || + window.start >= window.end + ) { + throw new Error("Invalid task events search projection window"); + } +} + +function toClickHouseDateTime64(value: Date): string { + return value.toISOString().replace("T", " ").replace("Z", ""); +} diff --git a/internal-packages/database/prisma/migrations/20260814070000_add_logs_search_projector_checkpoints/migration.sql b/internal-packages/database/prisma/migrations/20260814070000_add_logs_search_projector_checkpoints/migration.sql new file mode 100644 index 00000000000..e078b672af1 --- /dev/null +++ b/internal-packages/database/prisma/migrations/20260814070000_add_logs_search_projector_checkpoints/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "public"."LogsSearchProjectorCheckpoint" ( + "id" BIGSERIAL NOT NULL, + "projectorId" TEXT NOT NULL, + "mode" TEXT NOT NULL, + "windowStart" TIMESTAMP(3) NOT NULL, + "windowEnd" TIMESTAMP(3) NOT NULL, + "queryId" TEXT, + "completedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "LogsSearchProjectorCheckpoint_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "LogsSearchProjectorCheckpoint_projectorId_mode_windowStart__key" +ON "public"."LogsSearchProjectorCheckpoint"("projectorId", "mode", "windowStart", "windowEnd"); + +-- CreateIndex +CREATE INDEX "LogsSearchProjectorCheckpoint_projectorId_mode_windowEnd_idx" +ON "public"."LogsSearchProjectorCheckpoint"("projectorId", "mode", "windowEnd" DESC); diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 9c45d7fa7c0..1b5447b0ff1 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -3222,6 +3222,20 @@ model PlatformNotificationInteraction { @@unique([notificationId, userId]) } +model LogsSearchProjectorCheckpoint { + id BigInt @id @default(autoincrement()) + + projectorId String + mode String + windowStart DateTime + windowEnd DateTime + queryId String? + completedAt DateTime @default(now()) + + @@unique([projectorId, mode, windowStart, windowEnd]) + @@index([projectorId, mode, windowEnd(sort: Desc)]) +} + enum ErrorGroupStatus { UNRESOLVED RESOLVED From 2496a8a86360e8b2f35d745c997dfb10c35edb5a Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:03:12 +0100 Subject: [PATCH 11/98] feat(supervisor): optional image registry rewrite for run pods Adds two optional env vars that rewrite the registry host of run pod images at pod creation, so a supervisor can pull from a registry in its own region. Off by default and inert unless both are set. Exact host-prefix matching, so look-alike hosts pass through untouched. --- apps/supervisor/src/env.ts | 2 + .../src/workloadManager/imageRegistry.test.ts | 42 +++++++++++++++++++ .../src/workloadManager/imageRegistry.ts | 15 +++++++ .../src/workloadManager/kubernetes.ts | 7 +++- 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 apps/supervisor/src/workloadManager/imageRegistry.test.ts create mode 100644 apps/supervisor/src/workloadManager/imageRegistry.ts diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index fc18b7f09d8..3f2cc8b46e9 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -185,6 +185,8 @@ export const Env = z KUBERNETES_EPHEMERAL_STORAGE_SIZE_LIMIT: z.string().default("10Gi"), KUBERNETES_EPHEMERAL_STORAGE_SIZE_REQUEST: z.string().default("2Gi"), KUBERNETES_STRIP_IMAGE_DIGEST: BoolEnv.default(false), + KUBERNETES_IMAGE_REGISTRY_REWRITE_FROM: z.string().optional(), + KUBERNETES_IMAGE_REGISTRY_REWRITE_TO: z.string().optional(), KUBERNETES_CPU_REQUEST_MIN_CORES: z.coerce.number().min(0).default(0), KUBERNETES_CPU_REQUEST_RATIO: z.coerce.number().min(0).max(1).default(0.75), // Ratio of CPU limit, so 0.75 = 75% of CPU limit KUBERNETES_MEMORY_REQUEST_MIN_GB: z.coerce.number().min(0).default(0), diff --git a/apps/supervisor/src/workloadManager/imageRegistry.test.ts b/apps/supervisor/src/workloadManager/imageRegistry.test.ts new file mode 100644 index 00000000000..66f829564c3 --- /dev/null +++ b/apps/supervisor/src/workloadManager/imageRegistry.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { rewriteImageRegistry } from "./imageRegistry.js"; + +const FROM = "123456789012.dkr.ecr.us-east-1.amazonaws.com"; +const TO = "123456789012.dkr.ecr.eu-central-1.amazonaws.com"; + +describe("rewriteImageRegistry", () => { + it("rewrites the registry host and keeps the rest of the reference", () => { + expect(rewriteImageRegistry(`${FROM}/deployments/proj_abc:20260818.1`, FROM, TO)).toBe( + `${TO}/deployments/proj_abc:20260818.1` + ); + }); + + it("preserves a digest", () => { + expect(rewriteImageRegistry(`${FROM}/deployments/proj_abc@sha256:abc123`, FROM, TO)).toBe( + `${TO}/deployments/proj_abc@sha256:abc123` + ); + }); + + it("is a no-op unless both ends are configured", () => { + const ref = `${FROM}/deployments/proj_abc:tag`; + + expect(rewriteImageRegistry(ref, undefined, TO)).toBe(ref); + expect(rewriteImageRegistry(ref, FROM, undefined)).toBe(ref); + expect(rewriteImageRegistry(ref, undefined, undefined)).toBe(ref); + }); + + it("leaves other registries alone", () => { + const ref = "ghcr.io/triggerdotdev/something:tag"; + expect(rewriteImageRegistry(ref, FROM, TO)).toBe(ref); + }); + + it("only matches on a host boundary", () => { + const lookalike = `${FROM}.evil.example.com/deployments/proj_abc:tag`; + expect(rewriteImageRegistry(lookalike, FROM, TO)).toBe(lookalike); + }); + + it("does not rewrite a host that merely contains the source", () => { + const ref = `registry.example.com/${FROM}/proj_abc:tag`; + expect(rewriteImageRegistry(ref, FROM, TO)).toBe(ref); + }); +}); diff --git a/apps/supervisor/src/workloadManager/imageRegistry.ts b/apps/supervisor/src/workloadManager/imageRegistry.ts new file mode 100644 index 00000000000..a49da7f85f3 --- /dev/null +++ b/apps/supervisor/src/workloadManager/imageRegistry.ts @@ -0,0 +1,15 @@ +export function rewriteImageRegistry( + imageRef: string, + from: string | undefined, + to: string | undefined +): string { + if (!from || !to) { + return imageRef; + } + + if (!imageRef.startsWith(`${from}/`)) { + return imageRef; + } + + return `${to}${imageRef.slice(from.length)}`; +} diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index d09c86cf9cc..54a9428efeb 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -20,6 +20,7 @@ import { withRunnerSeccompProfile, withNodeSelector, } from "./kubernetesPodSpec.js"; +import { rewriteImageRegistry } from "./imageRegistry.js"; type ResourceQuantities = { [K in "cpu" | "memory" | "ephemeral-storage"]?: string; @@ -163,7 +164,11 @@ export class KubernetesWorkloadManager implements WorkloadManager { containers: [ { name: "run-controller", - image: this.stripImageDigest(opts.image), + image: rewriteImageRegistry( + this.stripImageDigest(opts.image), + env.KUBERNETES_IMAGE_REGISTRY_REWRITE_FROM, + env.KUBERNETES_IMAGE_REGISTRY_REWRITE_TO + ), ports: [ { containerPort: 8000, From 444c2215ca905c0fe7fead093ae3a6e67c891e1f Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 18 Aug 2026 18:32:41 +0100 Subject: [PATCH 12/98] fix(run-engine): stop requeued runs with a lapsed ttl being orphaned in the queue (#4669) ## Summary A run triggered with a `ttl` could get permanently stuck showing as queued. If the run started executing and was then requeued after a failure (a stalled heartbeat, a worker dying mid-run) once its TTL had already elapsed, the next dequeue pass silently dropped it from every queue structure. The run stayed QUEUED in the database forever, and nothing (dequeue, the TTL consumer, queue repair) could ever see it again. ## Root cause Enqueue registers a TTL entry for the TTL consumer, and the first dequeue removes it ("the run is executing, not expired"). A nack rewrote the message preserving the original `ttlExpiresAt` without re-registering that entry. The next dequeue pass then took the expired-TTL branch: remove the run from the queue sorted sets and leave the message for the TTL consumer to finalize. But the consumer's entry was gone, so nothing ever finalized the run. The fix has two halves: - `nackMessage` drops `ttlExpiresAt` from the rewritten message. TTL only applies to runs that have never been dequeued (the same contract as `includeTtl` on re-enqueues), so a requeued run stays dequeuable and is never expired by its original deadline. - The dequeue expired-TTL branches now (re-)register the TTL entry instead of assuming it exists, so any message still carrying a lapsed `ttlExpiresAt` with no TTL entry (including ones written before this fix) finalizes as EXPIRED instead of orphaning. ## Verification New engine test suite `ttlNackRequeue.test.ts` (testcontainers, real Redis and Postgres). All four tests fail before the fix and pass after: - a heartbeat-stalled EXECUTING run with a lapsed TTL is requeued and dequeued again instead of orphaned (the full production failure chain) - requeue-after-failure strips `ttlExpiresAt` so later dequeues do not treat the run as expired - a lapsed-TTL message whose TTL entry is missing is re-registered by dequeue and finalized as EXPIRED, for both plain and concurrency-key queues Also ran the existing ttl, heartbeats, dequeuing and attemptFailures engine suites plus the full run-queue suite (149 tests) against the change. --- .../ttl-runs-no-longer-stuck-after-requeue.md | 6 + .../src/engine/tests/ttlNackRequeue.test.ts | 528 ++++++++++++++++++ .../run-engine/src/run-queue/index.ts | 28 +- 3 files changed, 559 insertions(+), 3 deletions(-) create mode 100644 .server-changes/ttl-runs-no-longer-stuck-after-requeue.md create mode 100644 internal-packages/run-engine/src/engine/tests/ttlNackRequeue.test.ts diff --git a/.server-changes/ttl-runs-no-longer-stuck-after-requeue.md b/.server-changes/ttl-runs-no-longer-stuck-after-requeue.md new file mode 100644 index 00000000000..b167ecde8c0 --- /dev/null +++ b/.server-changes/ttl-runs-no-longer-stuck-after-requeue.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Runs triggered with a `ttl` could get permanently stuck in the queued state if they started executing and were then requeued after a failure (for example a worker dying mid-run) once the TTL had already elapsed. Requeued runs now dequeue normally: a run's TTL only applies while it is waiting to start for the first time. diff --git a/internal-packages/run-engine/src/engine/tests/ttlNackRequeue.test.ts b/internal-packages/run-engine/src/engine/tests/ttlNackRequeue.test.ts new file mode 100644 index 00000000000..c41104abf4d --- /dev/null +++ b/internal-packages/run-engine/src/engine/tests/ttlNackRequeue.test.ts @@ -0,0 +1,528 @@ +import { containerTest, assertNonNullable } from "@internal/testcontainers"; +import { trace } from "@internal/tracing"; +import { expect } from "vitest"; +import { RunEngine } from "../index.js"; +import { setTimeout } from "timers/promises"; +import type { EventBusEventArgs } from "../eventBus.js"; +import { setupAuthenticatedEnvironment, setupBackgroundWorker } from "./setup.js"; + +vi.setConfig({ testTimeout: 60_000 }); + +/** + * TTL interacts with nack/requeue in a dangerous way: enqueue registers a run in a + * TTL sorted set for the TTL consumer, and the first dequeue removes that entry + * ("the run is executing, not expired"). If the run is later nacked back onto the + * queue with its original (now lapsed) ttlExpiresAt still in the message, the next + * dequeue pass takes the expired-TTL branch: it removes the run from the queue + * sorted sets and defers finalization to a TTL consumer that no longer has any + * entry for the run. The run then exists in no queue structure at all — Postgres + * says QUEUED forever, and nothing (dequeue, TTL consumer, concurrency sweeper, + * repair) can ever see it again. + * + * These tests lock in the two halves of the fix: + * 1. nack strips ttlExpiresAt — TTL only applies to runs that have never been + * dequeued (the same contract as includeTtl on re-enqueues), so a requeued run + * stays dequeuable and is never expired by its original deadline. + * 2. The dequeue expired-TTL branch re-registers the TTL entry instead of assuming + * it exists, so any message still carrying a lapsed ttlExpiresAt with no TTL + * entry (e.g. written before the fix) finalizes as EXPIRED instead of orphaning. + */ +describe("RunEngine ttl + nack/requeue", () => { + containerTest( + "Heartbeat-stalled run with a lapsed TTL is requeued and dequeued again (not orphaned)", + async ({ prisma, redisOptions }) => { + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + + const executingTimeout = 200; + + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + processWorkerQueueDebounceMs: 50, + masterQueueConsumersDisabled: true, + retryOptions: { + maxAttempts: 12, + minTimeoutInMs: 50, + maxTimeoutInMs: 50, + factor: 1, + randomize: false, + }, + ttlSystem: { + pollIntervalMs: 100, + batchSize: 10, + batchMaxWaitMs: 100, + }, + }, + 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, + }, + heartbeatTimeoutsMs: { + EXECUTING: executingTimeout, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); + + try { + const taskIdentifier = "test-task"; + + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + + const expiredEvents: EventBusEventArgs<"runExpired">[0][] = []; + engine.eventBus.on("runExpired", (result) => { + expiredEvents.push(result); + }); + + const triggeredAt = Date.now(); + const run = await engine.trigger( + { + number: 1, + friendlyId: "run_stall1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t_stall1", + spanId: "s_stall1", + workerQueue: "main", + queue: "task/test-task", + isTest: false, + tags: [], + ttl: "1s", + }, + prisma + ); + + await setTimeout(500); + const dequeued = await engine.dequeueFromWorkerQueue({ + consumerId: "test_stall1", + workerQueue: "main", + }); + expect(dequeued.length).toBe(1); + + await engine.startRunAttempt({ + runId: dequeued[0].run.id, + snapshotId: dequeued[0].snapshot.id, + }); + + const executionData = await engine.getRunExecutionData({ runId: run.id }); + assertNonNullable(executionData); + expect(executionData.snapshot.executionStatus).toBe("EXECUTING"); + + await vi.waitFor( + async () => { + const data = await engine.getRunExecutionData({ runId: run.id }); + assertNonNullable(data); + expect(data.snapshot.executionStatus).toBe("QUEUED"); + }, + { timeout: 10_000, interval: 100 } + ); + + const pastDeadlineMs = triggeredAt + 1_000 + 300 - Date.now(); + if (pastDeadlineMs > 0) { + await setTimeout(pastDeadlineMs); + } + + const message = await engine.runQueue.readMessage( + authenticatedEnvironment.organization.id, + run.id + ); + assertNonNullable(message); + expect(message.ttlExpiresAt).toBeUndefined(); + + await engine.runQueue.processMasterQueueForEnvironment(authenticatedEnvironment.id, 10); + const dequeued2 = await engine.dequeueFromWorkerQueue({ + consumerId: "test_stall1", + workerQueue: "main", + blockingPopTimeoutSeconds: 1, + }); + expect(dequeued2.length).toBe(1); + expect(dequeued2[0]?.run.id).toBe(run.id); + + expect(expiredEvents.length).toBe(0); + } finally { + await engine.quit(); + } + } + ); + + containerTest( + "Requeue after a failure strips ttlExpiresAt so later dequeues do not treat the run as expired", + async ({ prisma, redisOptions }) => { + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + processWorkerQueueDebounceMs: 50, + masterQueueConsumersDisabled: true, + retryOptions: { + maxAttempts: 12, + minTimeoutInMs: 50, + maxTimeoutInMs: 50, + factor: 1, + randomize: false, + }, + ttlSystem: { + pollIntervalMs: 100, + batchSize: 10, + batchMaxWaitMs: 100, + }, + }, + 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 expiredEvents: EventBusEventArgs<"runExpired">[0][] = []; + engine.eventBus.on("runExpired", (result) => { + expiredEvents.push(result); + }); + + const triggeredAt = Date.now(); + const run = await engine.trigger( + { + number: 1, + friendlyId: "run_nack1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t_nack1", + spanId: "s_nack1", + workerQueue: "main", + queue: "task/test-task", + isTest: false, + tags: [], + ttl: "1s", + }, + prisma + ); + + await setTimeout(500); + const dequeued = await engine.dequeueFromWorkerQueue({ + consumerId: "test_nack1", + workerQueue: "main", + }); + expect(dequeued.length).toBe(1); + + const nackResult = await engine.runAttemptSystem.tryNackAndRequeue({ + run: { id: run.id }, + environment: { + id: authenticatedEnvironment.id, + type: authenticatedEnvironment.type, + }, + orgId: authenticatedEnvironment.organization.id, + projectId: authenticatedEnvironment.project.id, + timestamp: Date.now(), + error: { + type: "INTERNAL_ERROR", + code: "TASK_RUN_DEQUEUED_MAX_RETRIES", + message: "test requeue", + }, + }); + expect(nackResult.wasRequeued).toBe(true); + + const message = await engine.runQueue.readMessage( + authenticatedEnvironment.organization.id, + run.id + ); + assertNonNullable(message); + expect(message.ttlExpiresAt).toBeUndefined(); + + const pastDeadlineMs = triggeredAt + 1_000 + 300 - Date.now(); + if (pastDeadlineMs > 0) { + await setTimeout(pastDeadlineMs); + } + + await engine.runQueue.processMasterQueueForEnvironment(authenticatedEnvironment.id, 10); + const dequeued2 = await engine.dequeueFromWorkerQueue({ + consumerId: "test_nack1", + workerQueue: "main", + blockingPopTimeoutSeconds: 1, + }); + expect(dequeued2.length).toBe(1); + expect(dequeued2[0]?.run.id).toBe(run.id); + + expect(expiredEvents.length).toBe(0); + } finally { + await engine.quit(); + } + } + ); + + containerTest( + "Dequeue re-registers a lapsed-TTL message for the TTL consumer when its TTL entry is missing", + async ({ prisma, redisOptions }) => { + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + + const engine = new RunEngine({ + prisma, + worker: { + disabled: true, + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + processWorkerQueueDebounceMs: 50, + masterQueueConsumersDisabled: true, + ttlSystem: { + pollIntervalMs: 100, + batchSize: 10, + batchMaxWaitMs: 100, + }, + }, + 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_lostttl1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t_lost1", + spanId: "s_lost1", + workerQueue: "main", + queue: "task/test-task", + isTest: false, + tags: [], + ttl: "1s", + }, + prisma + ); + + const message = await engine.runQueue.readMessage( + authenticatedEnvironment.organization.id, + run.id + ); + assertNonNullable(message); + expect(message.ttlExpiresAt).toBeDefined(); + + const ttlMember = `${message.queue}|${run.id}|${authenticatedEnvironment.organization.id}`; + let removed = 0; + for (let shard = 0; shard < 4; shard++) { + removed += await engine.runQueue.redis.zrem( + engine.runQueue.keys.ttlQueueKeyForShard(shard), + ttlMember + ); + } + expect(removed).toBe(1); + + await setTimeout(1_300); + + await engine.runQueue.processMasterQueueForEnvironment(authenticatedEnvironment.id, 10); + + await vi.waitFor( + async () => { + const expiredRun = await prisma.taskRun.findUnique({ + where: { id: run.id }, + select: { status: true }, + }); + expect(expiredRun?.status).toBe("EXPIRED"); + }, + { timeout: 15_000, interval: 200 } + ); + + const messageExists = await engine.runQueue.messageExists( + authenticatedEnvironment.organization.id, + run.id + ); + expect(messageExists).toBe(0); + + const envConcurrency = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); + expect(envConcurrency).toBe(0); + } finally { + await engine.quit(); + } + } + ); + + containerTest( + "Dequeue re-registers a lapsed-TTL message with a concurrency key when its TTL entry is missing", + async ({ prisma, redisOptions }) => { + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + + const engine = new RunEngine({ + prisma, + worker: { + disabled: true, + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + processWorkerQueueDebounceMs: 50, + masterQueueConsumersDisabled: true, + ttlSystem: { + pollIntervalMs: 100, + batchSize: 10, + batchMaxWaitMs: 100, + }, + }, + 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_lostttl2", + environment: authenticatedEnvironment, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t_lost2", + spanId: "s_lost2", + workerQueue: "main", + queue: "task/test-task", + isTest: false, + tags: [], + ttl: "1s", + concurrencyKey: "ckA", + }, + prisma + ); + + const message = await engine.runQueue.readMessage( + authenticatedEnvironment.organization.id, + run.id + ); + assertNonNullable(message); + expect(message.ttlExpiresAt).toBeDefined(); + expect(message.concurrencyKey).toBeDefined(); + + const ttlMember = `${message.queue}|${run.id}|${authenticatedEnvironment.organization.id}`; + let removed = 0; + for (let shard = 0; shard < 4; shard++) { + removed += await engine.runQueue.redis.zrem( + engine.runQueue.keys.ttlQueueKeyForShard(shard), + ttlMember + ); + } + expect(removed).toBe(1); + + await setTimeout(1_300); + + await engine.runQueue.processMasterQueueForEnvironment(authenticatedEnvironment.id, 10); + + await vi.waitFor( + async () => { + const expiredRun = await prisma.taskRun.findUnique({ + where: { id: run.id }, + select: { status: true }, + }); + expect(expiredRun?.status).toBe("EXPIRED"); + }, + { timeout: 15_000, interval: 200 } + ); + + const messageExists = await engine.runQueue.messageExists( + authenticatedEnvironment.organization.id, + run.id + ); + expect(messageExists).toBe(0); + } finally { + await engine.quit(); + } + } + ); +}); diff --git a/internal-packages/run-engine/src/run-queue/index.ts b/internal-packages/run-engine/src/run-queue/index.ts index 57cfe518f37..1edc83380e4 100644 --- a/internal-packages/run-engine/src/run-queue/index.ts +++ b/internal-packages/run-engine/src/run-queue/index.ts @@ -1104,6 +1104,12 @@ export class RunQueue { /** * Negative acknowledge a message, which will requeue the message (with an optional future date). If you pass no date it will get reattempted with exponential backoff. + + The rewritten message drops ttlExpiresAt: TTL only applies to runs that have never been + dequeued, and a nack is always post-dequeue (the run's TTL set entry was already removed + at dequeue time). Carrying a lapsed ttlExpiresAt forward would make the next dequeue pass + treat the requeued run as expired and drop it from the queue sorted sets, deferring to a + TTL consumer that has no entry for it — orphaning the run. */ public async nackMessage({ orgId, @@ -1151,6 +1157,8 @@ export class RunQueue { } } + delete message.ttlExpiresAt; + if (!skipDequeueProcessing) { // For CK queues, use wildcard dedup so all CKs share one worker queue processing job const dedupQueueKey = message.concurrencyKey @@ -4290,10 +4298,15 @@ for i = 1, #messages, 2 do -- Check if TTL has expired if ttlExpiresAt and ttlExpiresAt <= currentTime then -- TTL expired - remove from dequeue queues so it won't be retried, - -- but leave messageKey and ttlQueueKey intact for the TTL consumer - -- to discover and properly expire the run. + -- leave messageKey intact, and (re-)register the TTL entry so the + -- TTL consumer can discover and properly expire the run. The entry + -- is removed on first dequeue, so it cannot be assumed to exist. redis.call('ZREM', queueKey, messageId) redis.call('ZREM', envQueueKey, messageId) + if ttlQueueKey and ttlQueueKey ~= '' then + local ttlMember = queueName .. '|' .. messageId .. '|' .. (messageData.orgId or '') + redis.call('ZADD', ttlQueueKey, ttlExpiresAt, ttlMember) + end else -- Not expired - process normally redis.call('ZREM', queueKey, messageId) @@ -4423,9 +4436,14 @@ for _, ckQueueName in ipairs(ckQueues) do local ttlExpiresAt = messageData and messageData.ttlExpiresAt if ttlExpiresAt and ttlExpiresAt <= currentTime then - -- TTL expired - remove from queues + -- TTL expired - remove from queues and (re-)register the TTL entry + -- so the TTL consumer can discover and properly expire the run redis.call('ZREM', fullQueueKey, messageId) redis.call('ZREM', envQueueKey, messageId) + if ttlQueueKey and ttlQueueKey ~= '' then + local ttlMember = ckQueueName .. '|' .. messageId .. '|' .. (messageData.orgId or '') + redis.call('ZADD', ttlQueueKey, ttlExpiresAt, ttlMember) + end else -- Dequeue normally redis.call('ZREM', fullQueueKey, messageId) @@ -4578,6 +4596,10 @@ for _, ckQueueName in ipairs(ckQueues) do redis.call('ZREM', fullQueueKey, messageId) redis.call('ZREM', envQueueKey, messageId) decrLengthCounter() + if ttlQueueKey and ttlQueueKey ~= '' then + local ttlMember = ckQueueName .. '|' .. messageId .. '|' .. (messageData.orgId or '') + redis.call('ZADD', ttlQueueKey, ttlExpiresAt, ttlMember) + end else redis.call('ZREM', fullQueueKey, messageId) redis.call('ZREM', envQueueKey, messageId) From e91fb746f7784f4583967c1b0d2f2123366ba064 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:40:18 +0100 Subject: [PATCH 13/98] feat(supervisor): configurable security context for run pods Adds KUBERNETES_RUNNER_SECURITY_CONTEXT (off | baseline | restricted), selecting how constrained the run container is. baseline drops the capability bounding set and blocks privilege escalation. restricted additionally pins the container to a non-root uid, chosen by runtime so bun images get their own. Default is off, so this is inert on merge. --- apps/supervisor/src/env.ts | 2 ++ .../src/workloadManager/kubernetes.test.ts | 33 +++++++++++++++++++ .../src/workloadManager/kubernetes.ts | 6 ++++ .../src/workloadManager/kubernetesPodSpec.ts | 30 +++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index 3f2cc8b46e9..e066ae31348 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -220,6 +220,8 @@ export const Env = z KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES: z .enum(["none", "node-24-plus", "all"]) .default("node-24-plus"), + KUBERNETES_RUNNER_SECURITY_CONTEXT: z.enum(["off", "baseline", "restricted"]).default("off"), + KUBERNETES_RUNNER_RUN_AS_USER: z.coerce.number().int().min(1).default(1000), // Pod DNS config — override the cluster default ndots to `KUBERNETES_POD_DNS_NDOTS`. // Default k8s ndots is 5: any name with fewer than 5 dots (e.g. `api.example.com`, 2 dots) is first walked diff --git a/apps/supervisor/src/workloadManager/kubernetes.test.ts b/apps/supervisor/src/workloadManager/kubernetes.test.ts index e3023bc5d84..e99e292aca1 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.test.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { nodetypeNodeSelector, runPodTolerations, + runnerSecurityContext, withRunnerSeccompProfile, withNodeSelector, } from "./kubernetesPodSpec.js"; @@ -153,3 +154,35 @@ describe("withRunnerSeccompProfile", () => { } }); }); + +describe("runnerSecurityContext", () => { + it("sets nothing when off", () => { + expect(runnerSecurityContext("off", 1000, "node-24")).toBeUndefined(); + }); + + it("drops all capabilities and blocks escalation at baseline", () => { + expect(runnerSecurityContext("baseline", 1000, "node-24")).toEqual({ + allowPrivilegeEscalation: false, + capabilities: { drop: ["ALL"] }, + }); + }); + + it("pins the configured uid when restricted", () => { + expect(runnerSecurityContext("restricted", 1000, "node-24")).toEqual({ + allowPrivilegeEscalation: false, + capabilities: { drop: ["ALL"] }, + runAsNonRoot: true, + runAsUser: 1000, + }); + }); + + it("pins bun's own uid, which differs from node's", () => { + expect(runnerSecurityContext("restricted", 1000, "bun")?.runAsUser).toBe(1001); + }); + + it("falls back to the configured uid when the runtime is unknown", () => { + for (const runtime of [undefined, null, "", "node", "node-22", "node-26"]) { + expect(runnerSecurityContext("restricted", 1000, runtime)?.runAsUser).toBe(1000); + } + }); +}); diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index 54a9428efeb..b7a2f752196 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -17,6 +17,7 @@ import { getRunnerId } from "../util.js"; import { nodetypeNodeSelector, runPodTolerations, + runnerSecurityContext, withRunnerSeccompProfile, withNodeSelector, } from "./kubernetesPodSpec.js"; @@ -175,6 +176,11 @@ export class KubernetesWorkloadManager implements WorkloadManager { }, ], resources: this.#getResourcesForMachine(opts.machine), + securityContext: runnerSecurityContext( + env.KUBERNETES_RUNNER_SECURITY_CONTEXT, + env.KUBERNETES_RUNNER_RUN_AS_USER, + opts.runtime + ), env: [ { name: "TRIGGER_DEQUEUED_AT_MS", diff --git a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts index 6c6ddfc09b0..ddd336d2f2e 100644 --- a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts +++ b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts @@ -96,3 +96,33 @@ export function withRunnerSeccompProfile( }, }; } + +const BUN_RUN_AS_USER = 1001; + +/** + * runnerSecurityContext maps a configured level onto the run container's security + * context. "baseline" drops the capability bounding set and blocks setuid + * escalation; "restricted" additionally pins the container to a non-root uid. + * + * The uid is set explicitly rather than read from the image: the kubelet cannot + * verify `runAsNonRoot` against an image that declares a named user, and fails + * the container instead. Bun images carry their user at a different uid to + * node's, so the runtime selects which uid is pinned. + */ +export function runnerSecurityContext( + level: "off" | "baseline" | "restricted", + runAsUser: number, + runtime: string | null | undefined +): k8s.V1SecurityContext | undefined { + if (level === "off") { + return undefined; + } + + return { + allowPrivilegeEscalation: false, + capabilities: { drop: ["ALL"] }, + ...(level === "restricted" + ? { runAsNonRoot: true, runAsUser: runtime === "bun" ? BUN_RUN_AS_USER : runAsUser } + : {}), + }; +} From d7056a9c673a32fc23e0f519563c1aa45f4d8871 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:50:32 +0100 Subject: [PATCH 14/98] chore(webapp): reword the no-billing-limit banner copy (#4656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _Requested by **Matt Aitken** · [Slack thread](https://triggerdotdev.slack.com/archives/C0BKB98B84W/p1787045331358929)_ Copy-only reword of the banner shown to org admins who have not set a billing limit yet. **Before** — the banner read "Protect your organization from unexpected usage spikes." with a button labelled "Configure billing limit". **After** — it reads "Add a billing limit to your account to prevent overspending" with a button labelled "Billing limit settings". The new wording names the action up front and matches the destination it sends you to, so the banner reads as a settings link rather than a one-off setup step. ## ✅ Checklist - [x] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) - [x] The PR title follows the convention. - [x] I ran and tested the code works --- ## Testing Formatting and linting pass (`oxfmt --check`, `oxlint`). No tests or snapshots assert this copy. The change is two string literals in one component, with no behaviour attached. --- ## Changelog Reworded the billing-limit banner for organizations without a limit configured, and relabelled its button to "Billing limit settings". --- ## How Both strings live in `NoLimitConfiguredBanner` in `apps/webapp/app/components/billing/OrgBanner.tsx`: the heading is the `canManageBillingLimits` branch of the banner's children, and the label is the `` inside the `LinkButton`. Only those two literals changed. The button still points at `v3BillingLimitsPath(organization)` (`/orgs/{slug}/settings/billing-limits`), so routing, permissions and the non-admin variant of the message are untouched. Co-authored-by: Claude --- apps/webapp/app/components/billing/OrgBanner.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/billing/OrgBanner.tsx b/apps/webapp/app/components/billing/OrgBanner.tsx index 03214bab7ec..acf10f2469d 100644 --- a/apps/webapp/app/components/billing/OrgBanner.tsx +++ b/apps/webapp/app/components/billing/OrgBanner.tsx @@ -157,14 +157,14 @@ function NoLimitConfiguredBanner() { to={v3BillingLimitsPath(organization)} > - Configure billing limit + Billing limit settings ) : undefined } > {canManageBillingLimits - ? "Protect your organization from unexpected usage spikes." + ? "Add a billing limit to your account to prevent overspending" : "Billing limits are not configured for this organization. Contact an organization administrator to configure them."} ); From b83cf671de036c96c4a251fceeb0e327b9c589bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Nerl=C3=B8e?= <32075361+NERLOE@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:59:00 +0200 Subject: [PATCH 15/98] fix(core): mint the fallback external trace id per run (#4534) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Runs that carry no external trace context (schedules, task-to-task triggers) fall back to a trace id generated once in the [`TracingSDK` constructor](https://github.com/triggerdotdev/trigger.dev/blob/main/packages/core/src/v3/otel/tracingSDK.ts#L165). With `experimental_processKeepAlive` the SDK outlives the run, so every run on a warm process is exported to the external OTLP endpoint under that one id. Across our production traces, 80.3% contained spans from more than one run, worst case 25. Per-run cost and latency attribution is unusable as a result. This is the same warm-start hazard c043c4a6a fixed for the external-context path, which left the fallback captured at construction. ## How `FallbackExternalTraceIds` hands out one id per internal trace, shared by the span and log wrappers so a run's spans and logs agree. The id is keyed off the record's own internal trace id rather than ambient state at export time, because batch processors drain asynchronously and a run's records routinely export after the next run has started. The map is bounded and evicts least-recently-used, so a run that is still exporting can't lose its id. Granularity follows the internal trace, so a run and the runs it triggers stay on one trace. **Risk:** the wrappers only exist when `exporters` / `logExporters` are configured, so deployments that don't export externally are untouched. Nothing outside `tracingSDK.ts` changes. **Known gap (pre-existing):** sampling and id selection still branch on ambient `getExternalTraceContext()`, so records draining across a run boundary in mixed mode are misplaced in both directions. It can't use the approach here — the external id comes from the run's incoming `traceparent`, which isn't carried on the record — so closing it means capturing `internalTraceId -> external context` in a span processor. Happy to follow up separately. --- ## Testing `packages/core` suite passes. `pnpm run format` and `pnpm run lint:fix` produce no diff. Six cases in `externalSpanExporterWrapper.test.ts`, each mutation-checked rather than just observed passing: one id per run, stability within a run, correct id when records drain after the next run started (spans and logs together), external export stays off when unconfigured, retention of a run still exporting while the map churns, and the bound itself. **CI:** the five failing `webapp` shards are the ones containing `containerTest` suites. Fork PRs receive no repository secrets, so `unit-tests-webapp.yml` skips the DockerHub login and the image pre-pull (both gated on `env.DOCKERHUB_USERNAME`) and the container tests time out at 60s. Same five shards across five runs, every failure a 60s timeout, and those shards pass on internal PRs. Happy to be corrected if you can run them with secrets available. --- ## Changelog Unrelated runs are no longer merged into a single trace in your external observability tool when they happen to execute on the same warm worker process. A run and the runs it triggers still share one trace, so a run tree stays together. --- ## Screenshots _n/a_ --- _Supersedes #4526 (auto-closed before I was vouched) and #4533 (opened ready rather than as a draft). GitHub won't reopen either._ --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Iss <74388823+isshaddad@users.noreply.github.com> Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com> --- .changeset/external-trace-id-per-run.md | 5 + packages/core/src/v3/otel/tracingSDK.ts | 110 +++++++-- .../test/externalSpanExporterWrapper.test.ts | 216 +++++++++++++++++- 3 files changed, 311 insertions(+), 20 deletions(-) create mode 100644 .changeset/external-trace-id-per-run.md diff --git a/.changeset/external-trace-id-per-run.md b/.changeset/external-trace-id-per-run.md new file mode 100644 index 00000000000..2ba004f4a2c --- /dev/null +++ b/.changeset/external-trace-id-per-run.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/core": patch +--- + +Unrelated runs are no longer merged into a single trace in your external observability tool when they happen to execute on the same warm worker process. diff --git a/packages/core/src/v3/otel/tracingSDK.ts b/packages/core/src/v3/otel/tracingSDK.ts index 9f8de5b6676..1dace4a102a 100644 --- a/packages/core/src/v3/otel/tracingSDK.ts +++ b/packages/core/src/v3/otel/tracingSDK.ts @@ -163,12 +163,13 @@ export class TracingSDK { ) ); - const externalTraceId = idGenerator.generateTraceId(); + // Shared by every wrapper below so a run's spans and logs agree on the id. + const fallbackTraceIds = new FallbackExternalTraceIds(idGenerator.generateTraceId()); for (const exporter of config.exporters ?? []) { spanProcessors.push( getEnvVar("TRIGGER_OTEL_BATCH_PROCESSING_ENABLED") === "1" - ? new BatchSpanProcessor(new ExternalSpanExporterWrapper(exporter, externalTraceId), { + ? new BatchSpanProcessor(new ExternalSpanExporterWrapper(exporter, fallbackTraceIds), { maxExportBatchSize: parseInt( getEnvVar("TRIGGER_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE") ?? "64" ), @@ -180,7 +181,7 @@ export class TracingSDK { ), maxQueueSize: parseInt(getEnvVar("TRIGGER_OTEL_SPAN_MAX_QUEUE_SIZE") ?? "512"), }) - : new SimpleSpanProcessor(new ExternalSpanExporterWrapper(exporter, externalTraceId)) + : new SimpleSpanProcessor(new ExternalSpanExporterWrapper(exporter, fallbackTraceIds)) ); } @@ -232,7 +233,7 @@ export class TracingSDK { logProcessors.push( getEnvVar("TRIGGER_OTEL_BATCH_PROCESSING_ENABLED") === "1" ? new BatchLogRecordProcessor( - new ExternalLogRecordExporterWrapper(externalLogExporter, externalTraceId), + new ExternalLogRecordExporterWrapper(externalLogExporter, fallbackTraceIds), { maxExportBatchSize: parseInt( getEnvVar("TRIGGER_OTEL_LOG_MAX_EXPORT_BATCH_SIZE") ?? "64" @@ -247,7 +248,7 @@ export class TracingSDK { } ) : new SimpleLogRecordProcessor( - new ExternalLogRecordExporterWrapper(externalLogExporter, externalTraceId) + new ExternalLogRecordExporterWrapper(externalLogExporter, fallbackTraceIds) ) ); } @@ -424,10 +425,81 @@ function setLogLevel(level: TracingDiagnosticLogLevel) { diag.setLogger(new DiagConsoleLogger(), diagLogLevel); } +/** Only the current run and the tail of recently ended ones can still export. */ +export const MAX_TRACKED_INTERNAL_TRACES = 64; + +/** + * External trace ids for runs that carry no external trace context — with + * `processKeepAlive` the `TracingSDK` outlives the run, so an id captured at + * construction merges every run on the process into one trace. + * + * A record's id comes from its own internal trace id rather than from whatever + * run is current when the exporter is called. Batch processors drain + * asynchronously, so a run's records are routinely exported after the next run + * has started, and reading ambient state then would stamp them with the wrong + * run's id. It also makes a run's spans and logs agree without coordinating. + * + * Granularity therefore follows the internal trace, not the run: a run tree + * shares one internal trace, so a parent and the runs it triggers land on one + * external trace together, which is the grouping you want. + */ +export class FallbackExternalTraceIds { + private readonly byInternalTrace = new Map(); + + constructor( + private seed: string, + private traceIdGenerator: Pick = idGenerator + ) {} + + /** False when no external trace id was configured, i.e. external export is off. */ + get enabled(): boolean { + return !!this.seed; + } + + forInternalTrace(internalTraceId: string): string { + // An empty seed means external export is disabled — leave it that way + // rather than minting an id and switching the feature on. + if (!this.seed) { + return this.seed; + } + + const known = this.byInternalTrace.get(internalTraceId); + + if (known) { + // Re-insert so the map is ordered by last use rather than first. A run + // that is still exporting keeps its id even if enough unrelated traces + // appear alongside it to fill the map, which would otherwise split it + // across two external traces. + this.byInternalTrace.delete(internalTraceId); + this.byInternalTrace.set(internalTraceId, known); + + return known; + } + + // The first run reuses the id generated at construction, so the configured + // seed is not thrown away. + const traceId = + this.byInternalTrace.size === 0 ? this.seed : this.traceIdGenerator.generateTraceId(); + + this.byInternalTrace.set(internalTraceId, traceId); + + if (this.byInternalTrace.size > MAX_TRACKED_INTERNAL_TRACES) { + // Map iterates in insertion order, so this drops the least recently used. + const stalest = this.byInternalTrace.keys().next().value; + + if (stalest !== undefined) { + this.byInternalTrace.delete(stalest); + } + } + + return traceId; + } +} + export class ExternalSpanExporterWrapper { constructor( private underlyingExporter: SpanExporter, - private externalTraceId: string + private fallback: FallbackExternalTraceIds ) {} private transformSpan(span: ReadableSpan): ReadableSpan | undefined { @@ -438,7 +510,7 @@ export class ExternalSpanExporterWrapper { const isExternallySampled = externalTraceContext ? isTraceFlagSampled(externalTraceContext.traceFlags) - : !!this.externalTraceId; + : this.fallback.enabled; if (!isExternallySampled) { return; @@ -450,7 +522,7 @@ export class ExternalSpanExporterWrapper { const externalTraceId = externalTraceContext ? externalTraceContext.traceId - : this.externalTraceId; + : this.fallback.forInternalTrace(span.spanContext().traceId); const isAttemptSpan = span.attributes[SemanticInternalAttributes.SPAN_ATTEMPT]; @@ -508,10 +580,10 @@ export class ExternalSpanExporterWrapper { } } -class ExternalLogRecordExporterWrapper { +export class ExternalLogRecordExporterWrapper { constructor( private underlyingExporter: LogRecordExporter, - private externalTraceId: string + private fallback: FallbackExternalTraceIds ) {} export(logs: any[], resultCallback: (result: any) => void): void { @@ -519,7 +591,7 @@ class ExternalLogRecordExporterWrapper { const isExternallySampled = externalTraceContext ? isTraceFlagSampled(externalTraceContext.traceFlags) - : !!this.externalTraceId; + : this.fallback.enabled; if (!isExternallySampled) { this.underlyingExporter.export([], resultCallback); @@ -550,14 +622,20 @@ class ExternalLogRecordExporterWrapper { | { traceId: string; spanId: string; tracestate?: string; traceFlags: number } | undefined ): ReadableLogRecord { - // Capture externalTraceId for use within the proxy's scope. - // Use externalTraceContext.traceId if available, otherwise fall back to generated externalTraceId + // Without a spanContext there is no internal trace id to key the fallback + // on, and nothing to rewrite. + if (!logRecord.spanContext) { + return logRecord; + } + + // Capture externalTraceId for use within the proxy's scope. Use + // externalTraceContext.traceId if available, otherwise the id belonging to + // the run this record came from. const externalTraceId = externalTraceContext ? externalTraceContext.traceId - : this.externalTraceId; + : this.fallback.forInternalTrace(logRecord.spanContext.traceId); - // If there's no spanContext, or if the externalTraceId is not set, return the original logRecord. - if (!logRecord.spanContext || !externalTraceId) { + if (!externalTraceId) { return logRecord; } diff --git a/packages/core/test/externalSpanExporterWrapper.test.ts b/packages/core/test/externalSpanExporterWrapper.test.ts index 9b51653a1ec..5fb150de466 100644 --- a/packages/core/test/externalSpanExporterWrapper.test.ts +++ b/packages/core/test/externalSpanExporterWrapper.test.ts @@ -1,17 +1,27 @@ import { SpanKind, SpanStatusCode, TraceFlags } from "@opentelemetry/api"; +import type { LogRecordExporter, ReadableLogRecord } from "@opentelemetry/sdk-logs"; import type { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-node"; import { beforeEach, describe, expect, it } from "vitest"; -import { ExternalSpanExporterWrapper } from "../src/v3/otel/tracingSDK.js"; +import { + ExternalLogRecordExporterWrapper, + ExternalSpanExporterWrapper, + FallbackExternalTraceIds, + MAX_TRACKED_INTERNAL_TRACES, +} from "../src/v3/otel/tracingSDK.js"; import { SemanticInternalAttributes } from "../src/v3/semanticInternalAttributes.js"; import { traceContext } from "../src/v3/trace-context-api.js"; import { StandardTraceContextManager } from "../src/v3/traceContext/manager.js"; const TRACEPARENT_RUN_A = "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1111111111111111-01"; const TRACEPARENT_RUN_B = "00-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2222222222222222-01"; +const SEED = "ffffffffffffffffffffffffffffffff"; +// Every span and log record of one run shares the run's internal trace id. +const INTERNAL_TRACE_RUN_A = "cccccccccccccccccccccccccccccccc"; +const INTERNAL_TRACE_RUN_B = "dddddddddddddddddddddddddddddddd"; -function createAttemptSpan(): ReadableSpan { +function createAttemptSpan(internalTraceId = INTERNAL_TRACE_RUN_A): ReadableSpan { const spanCtx = { - traceId: "cccccccccccccccccccccccccccccccc", + traceId: internalTraceId, spanId: "3333333333333333", traceFlags: TraceFlags.SAMPLED, }; @@ -36,6 +46,18 @@ function createAttemptSpan(): ReadableSpan { } as unknown as ReadableSpan; } +function createLogRecord(internalTraceId = INTERNAL_TRACE_RUN_A): ReadableLogRecord { + return { + body: "hello", + attributes: {}, + spanContext: { + traceId: internalTraceId, + spanId: "3333333333333333", + traceFlags: TraceFlags.SAMPLED, + }, + } as unknown as ReadableLogRecord; +} + function makeCapturingExporter(): { exporter: SpanExporter; captured: ReadableSpan[][] } { const captured: ReadableSpan[][] = []; const exporter: SpanExporter = { @@ -49,10 +71,40 @@ function makeCapturingExporter(): { exporter: SpanExporter; captured: ReadableSp return { exporter, captured }; } +function makeCapturingLogExporter(): { + exporter: LogRecordExporter; + captured: ReadableLogRecord[][]; +} { + const captured: ReadableLogRecord[][] = []; + const exporter: LogRecordExporter = { + export: (records, cb) => { + captured.push(records); + cb({ code: 0 } as any); + }, + shutdown: () => Promise.resolve(), + }; + return { exporter, captured }; +} + +/** Yields 000…001, 000…002, … so a reminted id is identifiable by its ordinal. */ +function makeIdGenerator() { + let generated = 0; + return { + generateTraceId: () => `${++generated}`.padStart(32, "0"), + get count() { + return generated; + }, + }; +} + describe("ExternalSpanExporterWrapper warm-start regression", () => { let manager: StandardTraceContextManager; beforeEach(() => { + // `setGlobalManager` delegates to `registerGlobal`, which ignores a second + // registration — without disabling first, every test after the first would + // keep mutating the first test's manager. + traceContext.disable(); manager = new StandardTraceContextManager(); traceContext.setGlobalManager(manager); }); @@ -62,7 +114,7 @@ describe("ExternalSpanExporterWrapper warm-start regression", () => { manager.traceContext = { external: { traceparent: TRACEPARENT_RUN_A } }; - const wrapper = new ExternalSpanExporterWrapper(exporter, "ffffffffffffffffffffffffffffffff"); + const wrapper = new ExternalSpanExporterWrapper(exporter, new FallbackExternalTraceIds(SEED)); manager.traceContext = { external: { traceparent: TRACEPARENT_RUN_B } }; @@ -77,4 +129,160 @@ describe("ExternalSpanExporterWrapper warm-start regression", () => { expect(span.parentSpanContext?.traceId).toBe("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); expect(span.spanContext().traceId).toBe("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); }); + + // Runs triggered internally — a schedule, or one task triggering another — + // carry no external trace context and so take the generated fallback. That id + // was captured at construction, which on a warm-started worker meant every run + // on the process shared a single trace id. + it("gives each run its own fallback trace id when there is no external context", () => { + const { exporter, captured } = makeCapturingExporter(); + const idGenerator = makeIdGenerator(); + + const wrapper = new ExternalSpanExporterWrapper( + exporter, + new FallbackExternalTraceIds(SEED, idGenerator) + ); + + wrapper.export([createAttemptSpan(INTERNAL_TRACE_RUN_A)], () => {}); + // A second run on the same warm process. + wrapper.export([createAttemptSpan(INTERNAL_TRACE_RUN_B)], () => {}); + + const runATraceId = captured[0]![0]!.spanContext().traceId; + const runBTraceId = captured[1]![0]!.spanContext().traceId; + + expect(runATraceId).toBe(SEED); + expect(runBTraceId).not.toBe(runATraceId); + expect(runBTraceId).toBe("00000000000000000000000000000001"); + }); + + it("keeps one fallback trace id across every export within a run", () => { + const { exporter, captured } = makeCapturingExporter(); + const idGenerator = makeIdGenerator(); + + const wrapper = new ExternalSpanExporterWrapper( + exporter, + new FallbackExternalTraceIds(SEED, idGenerator) + ); + + wrapper.export([createAttemptSpan()], () => {}); + wrapper.export([createAttemptSpan()], () => {}); + + expect(captured[1]![0]!.spanContext().traceId).toBe(captured[0]![0]!.spanContext().traceId); + expect(idGenerator.count).toBe(0); + }); + + // Batch processors drain asynchronously, so a run's records are routinely + // exported after the next run has already started. Deciding the id from + // ambient state at that moment would stamp the earlier run's records with the + // later run's id, merging exactly the traces this is meant to separate. + // + // Drives the span and log wrappers together: the TracingSDK shares one + // instance between them, and a run's spans and logs have to land on one trace. + it("stamps records with their own run's id even when exported after the next run started", () => { + const spans = makeCapturingExporter(); + const logs = makeCapturingLogExporter(); + const idGenerator = makeIdGenerator(); + + const fallback = new FallbackExternalTraceIds(SEED, idGenerator); + const spanWrapper = new ExternalSpanExporterWrapper(spans.exporter, fallback); + const logWrapper = new ExternalLogRecordExporterWrapper(logs.exporter, fallback); + + // Run B is underway and has already exported. Its ambient context has no + // `external` key, which is what a run on the fallback path looks like, so + // `getExternalTraceContext()` stays undefined throughout: the point is that + // the run currently in scope must not influence the records below at all. + spanWrapper.export([createAttemptSpan(INTERNAL_TRACE_RUN_B)], () => {}); + manager.traceContext = { traceparent: TRACEPARENT_RUN_B }; + + // Run A's queued records only drain now. + spanWrapper.export([createAttemptSpan(INTERNAL_TRACE_RUN_A)], () => {}); + logWrapper.export([createLogRecord(INTERNAL_TRACE_RUN_A)], () => {}); + + const runBTraceId = spans.captured[0]![0]!.spanContext().traceId; + const lateRunASpanId = spans.captured[1]![0]!.spanContext().traceId; + const lateRunALogId = logs.captured[0]![0]!.spanContext!.traceId; + + expect(lateRunASpanId).not.toBe(runBTraceId); + expect(lateRunALogId).toBe(lateRunASpanId); + }); + + it("leaves external export off when no external trace id was configured", () => { + const { exporter, captured } = makeCapturingExporter(); + const idGenerator = makeIdGenerator(); + + const wrapper = new ExternalSpanExporterWrapper( + exporter, + new FallbackExternalTraceIds("", idGenerator) + ); + + wrapper.export([createAttemptSpan()], () => {}); + + // Minting an id here would switch external export on for a deployment that + // never asked for it. + expect(captured[0]).toHaveLength(0); + }); + + // Instrumentation can start root spans outside a run's async context, each + // its own internal trace, so a run can be alive while the map churns. Evicting + // by insertion order would drop the run still using its id and split it across + // two external traces. + it("keeps the id of a run that is still exporting while other traces fill the map", () => { + const { exporter, captured } = makeCapturingExporter(); + + const wrapper = new ExternalSpanExporterWrapper( + exporter, + new FallbackExternalTraceIds(SEED, makeIdGenerator()) + ); + + const liveRun = "aa000000000000000000000000000000"; + wrapper.export([createAttemptSpan(liveRun)], () => {}); + + for (let i = 0; i < MAX_TRACKED_INTERNAL_TRACES * 2; i++) { + wrapper.export([createAttemptSpan(`bb${`${i}`.padStart(30, "0")}`)], () => {}); + // The run is still going, so it keeps exporting alongside the noise. + wrapper.export([createAttemptSpan(liveRun)], () => {}); + } + + expect(captured.at(-1)![0]!.spanContext().traceId).toBe(captured[0]![0]!.spanContext().traceId); + }); + + it("passes through a log record emitted outside a span, which has no spanContext", () => { + const logs = makeCapturingLogExporter(); + + const wrapper = new ExternalLogRecordExporterWrapper( + logs.exporter, + new FallbackExternalTraceIds(SEED) + ); + + const record = { body: "hello", attributes: {} } as unknown as ReadableLogRecord; + + expect(() => wrapper.export([record], () => {})).not.toThrow(); + expect(logs.captured[0]).toEqual([record]); + }); + + // A warm process is long-lived, so the map that remembers each run's id has + // to be bounded rather than growing for the life of the worker. + it("bounds how many runs it remembers", () => { + const { exporter, captured } = makeCapturingExporter(); + const idGenerator = makeIdGenerator(); + + const wrapper = new ExternalSpanExporterWrapper( + exporter, + new FallbackExternalTraceIds(SEED, idGenerator) + ); + + const firstRun = "aa000000000000000000000000000000"; + wrapper.export([createAttemptSpan(firstRun)], () => {}); + + for (let i = 0; i < MAX_TRACKED_INTERNAL_TRACES; i++) { + wrapper.export([createAttemptSpan(`bb${`${i}`.padStart(30, "0")}`)], () => {}); + } + + // Evicted, so it is treated as a run never seen before. + wrapper.export([createAttemptSpan(firstRun)], () => {}); + + expect(captured.at(-1)![0]!.spanContext().traceId).not.toBe( + captured[0]![0]!.spanContext().traceId + ); + }); }); From 12ec4667cb9ad35de9ac3f220bb85935a6a9b4ea Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 18 Aug 2026 19:30:21 +0100 Subject: [PATCH 16/98] feat(webapp): enable development branches for all organizations (#4670) --- apps/webapp/app/v3/featureFlags.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/webapp/app/v3/featureFlags.ts b/apps/webapp/app/v3/featureFlags.ts index 390892b121a..7c775799178 100644 --- a/apps/webapp/app/v3/featureFlags.ts +++ b/apps/webapp/app/v3/featureFlags.ts @@ -22,7 +22,6 @@ export const FEATURE_FLAG = { computeMigrationFreePercentage: "computeMigrationFreePercentage", computeMigrationPaidPercentage: "computeMigrationPaidPercentage", computeMigrationRequireTemplate: "computeMigrationRequireTemplate", - devBranchesEnabled: "devBranchesEnabled", runOpsMintKind: "runOpsMintKind", // Grace-linger stamp carried alongside runOpsMintKind on flip. See mintFlipGrace.ts. runOpsMintKindPrev: "runOpsMintKindPrev", @@ -83,8 +82,6 @@ export const FeatureFlagCatalog = { // When on, migrated orgs build their compute template in required mode at deploy // (fails the deploy on error) instead of shadow. Strict boolean (see above). [FEATURE_FLAG.computeMigrationRequireTemplate]: z.boolean(), - // Per-org access to development branches. Off unless enabled for the org. - [FEATURE_FLAG.devBranchesEnabled]: z.coerce.boolean(), // Per-org run-ops-id mint cutover. Defaults to "cuid"; only honored when // RUN_OPS_MINT_ENABLED is on AND isSplitEnabled() is true. [FEATURE_FLAG.runOpsMintKind]: z.enum(["cuid", "runOpsId"]), From cffaa05517c174a01dea1cafc4266d3eaa78e5bd Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:34:40 +0100 Subject: [PATCH 17/98] feat(supervisor): optional priority class for run pods (#4671) Adds an optional priority class for run pods. ``` KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME ``` When set, the value is applied as `priorityClassName` on the run pod spec. When unset, pods are created exactly as before. Off by default, and inert unless set. It sits beside the existing `KUBERNETES_SCHEDULER_NAME` option and follows the same conditional shape: ```ts ...(env.KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME ? { priorityClassName: env.KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME } : {}), ``` ## Verification `typecheck --filter supervisor`, `format` and `lint` clean. No changeset or `.server-changes/` note: off by default, no user-visible behaviour change. --- apps/supervisor/src/env.ts | 1 + apps/supervisor/src/workloadManager/kubernetes.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index e066ae31348..f15ef766753 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -187,6 +187,7 @@ export const Env = z KUBERNETES_STRIP_IMAGE_DIGEST: BoolEnv.default(false), KUBERNETES_IMAGE_REGISTRY_REWRITE_FROM: z.string().optional(), KUBERNETES_IMAGE_REGISTRY_REWRITE_TO: z.string().optional(), + KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME: z.string().optional(), KUBERNETES_CPU_REQUEST_MIN_CORES: z.coerce.number().min(0).default(0), KUBERNETES_CPU_REQUEST_RATIO: z.coerce.number().min(0).max(1).default(0.75), // Ratio of CPU limit, so 0.75 = 75% of CPU limit KUBERNETES_MEMORY_REQUEST_MIN_GB: z.coerce.number().min(0).default(0), diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index b7a2f752196..0394a8181e2 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -371,6 +371,11 @@ export class KubernetesWorkloadManager implements WorkloadManager { schedulerName: env.KUBERNETES_SCHEDULER_NAME, } : {}), + ...(env.KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME + ? { + priorityClassName: env.KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME, + } + : {}), ...nodetypeNodeSelector(env.KUBERNETES_WORKER_NODETYPE_LABEL), ...(env.KUBERNETES_POD_DNS_NDOTS_OVERRIDE_ENABLED ? { From fe1d5f6961581ea8438e7c0e45ef78fda0f40d8e Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 08:28:56 +0100 Subject: [PATCH 18/98] chore: enable additional correctness lint rules (#4672) ## Summary Enable additional lint rules that catch unsafe optional-chain assertions, inherited-property iteration, anonymous symbols, and unsafe external links. The existing violations now use explicit values and own-property checks, so the rules can prevent those patterns from returning. --- .oxlintrc.json | 5 +++- .../app/components/primitives/Callout.tsx | 1 + .../metadata/updateMetadata.server.ts | 2 +- .../routeBuilders/permissions.server.ts | 1 + apps/webapp/server.ts | 1 + packages/core/src/v3/apiClient/core.ts | 4 ++- packages/core/src/v3/errors.ts | 1 + packages/core/src/v3/locals/manager.ts | 2 +- packages/core/src/v3/serverOnly/httpServer.ts | 1 + .../core/src/v3/utils/flattenAttributes.ts | 1 + packages/trigger-sdk/src/v3/ai.ts | 27 +++++-------------- packages/trigger-sdk/src/v3/chat-client.ts | 11 +++----- 12 files changed, 26 insertions(+), 31 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index c51e04730cf..ce90235fed3 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -34,7 +34,7 @@ ], "no-empty-pattern": "off", "no-control-regex": "off", - "typescript/no-non-null-asserted-optional-chain": "off", + "typescript/no-non-null-asserted-optional-chain": "error", "no-unused-expressions": [ "error", { @@ -47,6 +47,9 @@ "import/namespace": "off", "react-hooks/exhaustive-deps": "off", "react-hooks/rules-of-hooks": "off", + "guard-for-in": "error", + "symbol-description": "error", + "react/jsx-no-target-blank": "error", "trigger/no-thrown-unawaited-redirect": "error", "trigger-prisma/no-unbounded-list-filter": "error", "trigger-prisma/no-unbounded-list-filter-in-args-helper": "error" diff --git a/apps/webapp/app/components/primitives/Callout.tsx b/apps/webapp/app/components/primitives/Callout.tsx index 6c772fb6e52..6bcd16de0a1 100644 --- a/apps/webapp/app/components/primitives/Callout.tsx +++ b/apps/webapp/app/components/primitives/Callout.tsx @@ -92,6 +92,7 @@ export function Callout({ ( ): Record { const result = {} as Record; for (const key in checks) { + if (!Object.hasOwn(checks, key)) continue; const check = checks[key]; result[key] = "requireSuper" in check ? ability.canSuper() : ability.can(check.action, check.resource); diff --git a/apps/webapp/server.ts b/apps/webapp/server.ts index 8bc6eeb5db2..51afe0c90ca 100644 --- a/apps/webapp/server.ts +++ b/apps/webapp/server.ts @@ -37,6 +37,7 @@ function installPrimarySignalHandlers() { const forward = (signal: NodeJS.Signals) => { for (const id in cluster.workers) { + if (!Object.hasOwn(cluster.workers, id)) continue; const w = cluster.workers[id]; if (w?.process?.pid) { try { diff --git a/packages/core/src/v3/apiClient/core.ts b/packages/core/src/v3/apiClient/core.ts index c6425e7ee37..a5f7f31bd7c 100644 --- a/packages/core/src/v3/apiClient/core.ts +++ b/packages/core/src/v3/apiClient/core.ts @@ -605,7 +605,9 @@ async function waitForRetry( // https://stackoverflow.com/a/34491287 export function isEmptyObj(obj: object | null | undefined): boolean { if (!obj) return true; - for (const _k in obj) return false; + for (const key in obj) { + if (Object.hasOwn(obj, key)) return false; + } return true; } diff --git a/packages/core/src/v3/errors.ts b/packages/core/src/v3/errors.ts index 1fc7c5bce6c..0c35485b47b 100644 --- a/packages/core/src/v3/errors.ts +++ b/packages/core/src/v3/errors.ts @@ -1172,6 +1172,7 @@ export function createTaskMetadataFailedErrorStack( const groupedIssues = groupTaskMetadataIssuesByTask(data.tasks, data.zodIssues); for (const key in groupedIssues) { + if (!Object.hasOwn(groupedIssues, key)) continue; const taskWithIssues = groupedIssues[key]; if (!taskWithIssues) { diff --git a/packages/core/src/v3/locals/manager.ts b/packages/core/src/v3/locals/manager.ts index 6f2157c98ff..d990b74f09d 100644 --- a/packages/core/src/v3/locals/manager.ts +++ b/packages/core/src/v3/locals/manager.ts @@ -3,7 +3,7 @@ import type { LocalsKey, LocalsManager } from "./types.js"; export class NoopLocalsManager implements LocalsManager { createLocal(id: string): LocalsKey { return { - __type: Symbol(), + __type: Symbol(id), id, }; } diff --git a/packages/core/src/v3/serverOnly/httpServer.ts b/packages/core/src/v3/serverOnly/httpServer.ts index 8360067d583..7e0e8895f78 100644 --- a/packages/core/src/v3/serverOnly/httpServer.ts +++ b/packages/core/src/v3/serverOnly/httpServer.ts @@ -346,6 +346,7 @@ export class HttpServer { private findRoute(url: string): string | null { for (const route in this.routes) { + if (!Object.hasOwn(this.routes, route)) continue; const routeParts = route.split("/"); const urlWithoutQueryParams = url.split("?")[0]; diff --git a/packages/core/src/v3/utils/flattenAttributes.ts b/packages/core/src/v3/utils/flattenAttributes.ts index 7852e855340..28af76215bf 100644 --- a/packages/core/src/v3/utils/flattenAttributes.ts +++ b/packages/core/src/v3/utils/flattenAttributes.ts @@ -346,6 +346,7 @@ export function unflattenAttributes( const maxIndex = Math.max(...Object.keys(result).map((k) => parseInt(k))); const arrayResult = Array(maxIndex + 1); for (const key in result) { + if (!Object.hasOwn(result, key)) continue; arrayResult[parseInt(key)] = result[key]; } return arrayResult as any; diff --git a/packages/trigger-sdk/src/v3/ai.ts b/packages/trigger-sdk/src/v3/ai.ts index 844d506079b..c241930323b 100644 --- a/packages/trigger-sdk/src/v3/ai.ts +++ b/packages/trigger-sdk/src/v3/ai.ts @@ -10444,6 +10444,10 @@ function createChatStartSessionAction( const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {}; + const maxAttempts = params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts; + const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration; + const idleTimeoutInSeconds = + params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds; const triggerConfig: SessionTriggerConfig = { basePayload: { @@ -10461,18 +10465,8 @@ function createChatStartSessionAction( ? { queue: params.triggerConfig?.queue ?? options?.triggerConfig?.queue } : {}), tags, - ...(options?.triggerConfig?.maxAttempts !== undefined || - params.triggerConfig?.maxAttempts !== undefined - ? { - maxAttempts: params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts!, - } - : {}), - ...(options?.triggerConfig?.maxDuration !== undefined || - params.triggerConfig?.maxDuration !== undefined - ? { - maxDuration: params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration!, - } - : {}), + ...(maxAttempts !== undefined ? { maxAttempts } : {}), + ...(maxDuration !== undefined ? { maxDuration } : {}), ...(options?.triggerConfig?.region || params.triggerConfig?.region ? { region: params.triggerConfig?.region ?? options?.triggerConfig?.region } : {}), @@ -10482,14 +10476,7 @@ function createChatStartSessionAction( params.triggerConfig?.lockToVersion ?? options?.triggerConfig?.lockToVersion, } : {}), - ...(options?.triggerConfig?.idleTimeoutInSeconds !== undefined || - params.triggerConfig?.idleTimeoutInSeconds !== undefined - ? { - idleTimeoutInSeconds: - params.triggerConfig?.idleTimeoutInSeconds ?? - options?.triggerConfig?.idleTimeoutInSeconds!, - } - : {}), + ...(idleTimeoutInSeconds !== undefined ? { idleTimeoutInSeconds } : {}), }; const startBody = { diff --git a/packages/trigger-sdk/src/v3/chat-client.ts b/packages/trigger-sdk/src/v3/chat-client.ts index 919d855e5e0..35cfd0b6af9 100644 --- a/packages/trigger-sdk/src/v3/chat-client.ts +++ b/packages/trigger-sdk/src/v3/chat-client.ts @@ -653,6 +653,9 @@ export class AgentChat { private async ensureStarted(options?: { idleTimeoutInSeconds?: number }): Promise { if (this.state.started) return; + const idleTimeoutInSeconds = + options?.idleTimeoutInSeconds ?? this.triggerConfigDefault?.idleTimeoutInSeconds; + const triggerConfig: SessionTriggerConfig = { basePayload: { // `trigger: "preload"` mirrors the browser-mediated @@ -672,13 +675,7 @@ export class AgentChat { ...(this.triggerConfigDefault?.maxAttempts !== undefined ? { maxAttempts: this.triggerConfigDefault.maxAttempts } : {}), - ...(options?.idleTimeoutInSeconds !== undefined || - this.triggerConfigDefault?.idleTimeoutInSeconds !== undefined - ? { - idleTimeoutInSeconds: - options?.idleTimeoutInSeconds ?? this.triggerConfigDefault?.idleTimeoutInSeconds!, - } - : {}), + ...(idleTimeoutInSeconds !== undefined ? { idleTimeoutInSeconds } : {}), }; const created = await sessions.start({ From 0f725cf2baff7ead37e71c62d310d8f209a615c7 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 08:28:57 +0100 Subject: [PATCH 19/98] chore: enable lint cleanup rules (#4673) ## Summary Enable small cleanup rules for redundant boolean expressions, object ownership checks, assignments, and object construction. The existing call sites now use the simpler equivalent forms, keeping future code consistent without changing behavior. Base: [#4672](https://github.com/triggerdotdev/trigger.dev/pull/4672) --- .oxlintrc.json | 11 +++++++++++ apps/webapp/app/components/billing/UsageBar.tsx | 2 +- .../app/components/primitives/TreeView/utils.ts | 2 +- apps/webapp/app/hooks/useThemeColor.ts | 3 ++- .../presenters/v3/ApiErrorListPresenter.server.ts | 4 +--- .../v3/ApiWebhookDeliveryPresenter.server.ts | 4 +--- apps/webapp/app/v3/eventRepository/common.server.ts | 2 +- .../app/v3/eventRepository/eventRepository.server.ts | 12 +++++------- .../app/v3/eventRepository/traceExport.server.ts | 2 +- .../v3/services/alerts/createAlertChannel.server.ts | 2 +- apps/webapp/memory-leak-detector.js | 10 ++++++---- internal-packages/run-store/src/PostgresRunStore.ts | 2 +- internal-packages/tsql/src/query/parser.ts | 2 +- packages/cli-v3/src/deploy/buildImage.ts | 8 ++++---- packages/core/src/v3/apiClient/core.ts | 2 +- packages/core/src/v3/workers/populateEnv.ts | 4 ++-- scripts/recover-stuck-runs.ts | 4 ++-- 17 files changed, 42 insertions(+), 34 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index ce90235fed3..4b0d1d4aed9 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -49,6 +49,11 @@ "react-hooks/rules-of-hooks": "off", "guard-for-in": "error", "symbol-description": "error", + "no-unneeded-ternary": "error", + "prefer-object-has-own": "error", + "no-redeclare": "error", + "no-multi-assign": "error", + "prefer-object-spread": "error", "react/jsx-no-target-blank": "error", "trigger/no-thrown-unawaited-redirect": "error", "trigger-prisma/no-unbounded-list-filter": "error", @@ -75,6 +80,12 @@ "trigger-prisma/no-unbounded-list-filter": "off", "trigger-prisma/no-unbounded-list-filter-in-args-helper": "off" } + }, + { + "files": ["internal-packages/tsql/**"], + "rules": { + "prefer-object-has-own": "off" + } } ] } diff --git a/apps/webapp/app/components/billing/UsageBar.tsx b/apps/webapp/app/components/billing/UsageBar.tsx index fcb6377757c..6cc5f6f93de 100644 --- a/apps/webapp/app/components/billing/UsageBar.tsx +++ b/apps/webapp/app/components/billing/UsageBar.tsx @@ -106,7 +106,7 @@ type LegendProps = { function Legend({ text, value, position, percentage, tooltipContent }: LegendProps) { const flipLegendPositionValue = 80; - const flipLegendPosition = percentage > flipLegendPositionValue ? true : false; + const flipLegendPosition = percentage > flipLegendPositionValue; return (
(tree: FlatTree, state: NodesState) const parent = node.parentId ? acc[node.parentId] : { selected: defaultSelected, expanded: defaultExpanded, visible: true }; - const visible = parent.expanded && parent.visible === true ? true : false; + const visible = parent.expanded && parent.visible === true; acc[node.id] = { ...nodeState, visible }; return acc; diff --git a/apps/webapp/app/hooks/useThemeColor.ts b/apps/webapp/app/hooks/useThemeColor.ts index f088d16b525..4555f6415ec 100644 --- a/apps/webapp/app/hooks/useThemeColor.ts +++ b/apps/webapp/app/hooks/useThemeColor.ts @@ -7,7 +7,8 @@ import { useEffect, useState } from "react"; */ function toRgb(color: string): string { const canvas = document.createElement("canvas"); - canvas.width = canvas.height = 1; + canvas.width = 1; + canvas.height = 1; const ctx = canvas.getContext("2d"); if (!ctx) return color; ctx.fillStyle = color; diff --git a/apps/webapp/app/presenters/v3/ApiErrorListPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiErrorListPresenter.server.ts index 13d794d6593..ee61fd3131a 100644 --- a/apps/webapp/app/presenters/v3/ApiErrorListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiErrorListPresenter.server.ts @@ -48,9 +48,7 @@ export const ApiErrorListSearchParams = z.object({ const statuses = value.split(","); // hasOwnProperty, not `in`: `in` walks the prototype chain, so // `filter[status]=toString` would pass and map to a function. - const invalid = statuses.filter( - (status) => !Object.prototype.hasOwnProperty.call(API_STATUS_TO_DB, status) - ); + const invalid = statuses.filter((status) => !Object.hasOwn(API_STATUS_TO_DB, status)); if (invalid.length > 0) { ctx.addIssue({ diff --git a/apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts index 7ebde680b62..0c6b2d1909b 100644 --- a/apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts @@ -55,9 +55,7 @@ export const ApiWebhookDeliveryListSearchParams = z.object({ .transform((value, ctx) => { if (!value) return undefined; const statuses = value.split(","); - const invalid = statuses.filter( - (s) => !Object.prototype.hasOwnProperty.call(API_STATUS_TO_DB, s) - ); + const invalid = statuses.filter((s) => !Object.hasOwn(API_STATUS_TO_DB, s)); if (invalid.length > 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, diff --git a/apps/webapp/app/v3/eventRepository/common.server.ts b/apps/webapp/app/v3/eventRepository/common.server.ts index eb8bf7f23ec..8f7fef29ad4 100644 --- a/apps/webapp/app/v3/eventRepository/common.server.ts +++ b/apps/webapp/app/v3/eventRepository/common.server.ts @@ -172,7 +172,7 @@ export function removePrivateProperties( export function isEmptyObject(obj: object) { for (var prop in obj) { - if (Object.prototype.hasOwnProperty.call(obj, prop)) { + if (Object.hasOwn(obj, prop)) { return false; } } diff --git a/apps/webapp/app/v3/eventRepository/eventRepository.server.ts b/apps/webapp/app/v3/eventRepository/eventRepository.server.ts index ea778fb5356..4a0dea3f4ba 100644 --- a/apps/webapp/app/v3/eventRepository/eventRepository.server.ts +++ b/apps/webapp/app/v3/eventRepository/eventRepository.server.ts @@ -1692,13 +1692,11 @@ function parseStyleField(style: Prisma.JsonValue): TaskEventStyle { } if (typeof unsafe === "object") { - return Object.assign( - { - icon: undefined, - variant: undefined, - }, - unsafe - ) as TaskEventStyle; + return { + icon: undefined, + variant: undefined, + ...unsafe, + } as TaskEventStyle; } return {}; diff --git a/apps/webapp/app/v3/eventRepository/traceExport.server.ts b/apps/webapp/app/v3/eventRepository/traceExport.server.ts index cea3dce65ba..b1aba4e1f95 100644 --- a/apps/webapp/app/v3/eventRepository/traceExport.server.ts +++ b/apps/webapp/app/v3/eventRepository/traceExport.server.ts @@ -199,7 +199,7 @@ const FORMATS: Record = { /** Resolve a `?format=` value to a format, defaulting to `log`. */ export function getTraceExportFormat(name: string | null | undefined): TraceExportFormat { - if (name && Object.prototype.hasOwnProperty.call(FORMATS, name)) { + if (name && Object.hasOwn(FORMATS, name)) { return FORMATS[name as TraceExportFormatName]; } return logFormat; diff --git a/apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts b/apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts index c9163667bed..50181de07b9 100644 --- a/apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts +++ b/apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts @@ -104,7 +104,7 @@ export class CreateAlertChannelService extends BaseService { properties: await this.#createProperties(options.channel), enabled: true, deduplicationKey: options.deduplicationKey, - userProvidedDeduplicationKey: options.deduplicationKey ? true : false, + userProvidedDeduplicationKey: Boolean(options.deduplicationKey), environmentTypes, }, }); diff --git a/apps/webapp/memory-leak-detector.js b/apps/webapp/memory-leak-detector.js index fafa55c84b3..7bec919d156 100644 --- a/apps/webapp/memory-leak-detector.js +++ b/apps/webapp/memory-leak-detector.js @@ -544,6 +544,8 @@ class MemoryLeakDetector { const snapshot3 = this.results.snapshots[2]; // after second load test let analysis = {}; + let heapGrowth; + let heapGrowthPercent; // Handle different snapshot types if ( @@ -592,8 +594,8 @@ class MemoryLeakDetector { }; // Use total growth for recommendations - var heapGrowth = totalGrowth; - var heapGrowthPercent = totalGrowthPercent; + heapGrowth = totalGrowth; + heapGrowthPercent = totalGrowthPercent; } else if (snapshot1.processMemory && snapshot2.processMemory && snapshot3.processMemory) { // Traditional process memory analysis with 3 snapshots const heap1 = snapshot1.processMemory.heapUsed; @@ -632,8 +634,8 @@ class MemoryLeakDetector { snapshots: this.results.snapshots.length, }; - var heapGrowth = totalHeapGrowth; - var heapGrowthPercent = (totalHeapGrowth / heap1) * 100; + heapGrowth = totalHeapGrowth; + heapGrowthPercent = (totalHeapGrowth / heap1) * 100; } else { this.log("Mixed or incompatible snapshot types - cannot analyze memory growth", "warn"); analysis = { diff --git a/internal-packages/run-store/src/PostgresRunStore.ts b/internal-packages/run-store/src/PostgresRunStore.ts index d11d51fb2c7..ec3943cb117 100644 --- a/internal-packages/run-store/src/PostgresRunStore.ts +++ b/internal-packages/run-store/src/PostgresRunStore.ts @@ -1950,7 +1950,7 @@ export class PostgresRunStore implements RunStore { ?.filter((c) => c.index !== undefined) .sort((a, b) => a.index! - b.index!) .map((w) => w.id), - isValid: error ? false : true, + isValid: !error, error, }, include: { checkpoint: true }, diff --git a/internal-packages/tsql/src/query/parser.ts b/internal-packages/tsql/src/query/parser.ts index 38e2689e915..f7c664555f5 100644 --- a/internal-packages/tsql/src/query/parser.ts +++ b/internal-packages/tsql/src/query/parser.ts @@ -1385,7 +1385,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor { } const args: Expression[] = ctx._columnArgList ? this.visitExprList(ctx._columnArgList) : []; - const distinct = ctx.DISTINCT() ? true : false; + const distinct = ctx.DISTINCT() !== undefined; return { expression_type: "call", name, params: parameters, args, distinct }; } diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 076ef271d1e..b213a0bc790 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -1158,11 +1158,11 @@ function shouldPush(imageTag: string, push?: boolean) { return false; } case undefined: { - return imageTag.startsWith("localhost") || + return !( + imageTag.startsWith("localhost") || imageTag.startsWith("127.0.0.1") || imageTag.startsWith("0.0.0.0") - ? false - : true; + ); } default: { assertExhaustive(push); @@ -1180,7 +1180,7 @@ function shouldLoad(load?: boolean, push?: boolean) { return false; } case undefined: { - return push ? false : true; + return !push; } default: { assertExhaustive(load); diff --git a/packages/core/src/v3/apiClient/core.ts b/packages/core/src/v3/apiClient/core.ts index a5f7f31bd7c..23fb5b36f79 100644 --- a/packages/core/src/v3/apiClient/core.ts +++ b/packages/core/src/v3/apiClient/core.ts @@ -613,7 +613,7 @@ export function isEmptyObj(obj: object | null | undefined): boolean { // https://eslint.org/docs/latest/rules/no-prototype-builtins export function hasOwn(obj: object, key: string): boolean { - return Object.prototype.hasOwnProperty.call(obj, key); + return Object.hasOwn(obj, key); } // If the requestInit has a header x-trigger-worker = true, then we will do diff --git a/packages/core/src/v3/workers/populateEnv.ts b/packages/core/src/v3/workers/populateEnv.ts index b21673c3fa6..5ec00c7d976 100644 --- a/packages/core/src/v3/workers/populateEnv.ts +++ b/packages/core/src/v3/workers/populateEnv.ts @@ -39,7 +39,7 @@ export function populateEnv( // Set process.env values for (const key of Object.keys(envObject)) { - if (Object.prototype.hasOwnProperty.call(process.env, key)) { + if (Object.hasOwn(process.env, key)) { if (override) { process.env[key] = envObject[key]; @@ -57,7 +57,7 @@ export function populateEnv( if (previousEnv) { // if there are any keys in previousEnv that are not in envObject, remove them from process.env for (const key of Object.keys(previousEnv)) { - if (!Object.prototype.hasOwnProperty.call(envObject, key)) { + if (!Object.hasOwn(envObject, key)) { delete process.env[key]; } } diff --git a/scripts/recover-stuck-runs.ts b/scripts/recover-stuck-runs.ts index 423e17bc07b..6df24682652 100755 --- a/scripts/recover-stuck-runs.ts +++ b/scripts/recover-stuck-runs.ts @@ -142,7 +142,7 @@ async function main() { ? { tls: { // If connecting via localhost tunnel to a remote Redis, disable cert verification - rejectUnauthorized: redisReadUrlObj.hostname === "localhost" ? false : true, + rejectUnauthorized: redisReadUrlObj.hostname !== "localhost", }, } : {}), @@ -165,7 +165,7 @@ async function main() { ? { tls: { // If connecting via localhost tunnel to a remote Redis, disable cert verification - rejectUnauthorized: redisWriteUrlObj.hostname === "localhost" ? false : true, + rejectUnauthorized: redisWriteUrlObj.hostname !== "localhost", }, } : {}), From b2afff252cd6d53cb9ae89cf1561b55873495f66 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 08:28:57 +0100 Subject: [PATCH 20/98] chore: enable JSX cleanup rules (#4674) ## Summary Enable JSX cleanup rules for shorthand fragments and self-closing components. The existing JSX is automatically simplified, and future components will follow the same concise form. Base: [#4673](https://github.com/triggerdotdev/trigger.dev/pull/4673) --- .oxlintrc.json | 2 ++ apps/webapp/app/components/code/CodeBlock.tsx | 2 +- apps/webapp/app/components/logs/LogsTable.tsx | 2 +- .../app/components/navigation/HelpAndFeedbackPopover.tsx | 6 +++--- apps/webapp/app/components/runs/v3/TaskRunsTable.tsx | 2 +- .../route.tsx | 7 +------ .../route.tsx | 2 +- .../route.tsx | 4 ++-- .../route.tsx | 2 +- apps/webapp/app/routes/admin.notifications.tsx | 2 +- apps/webapp/app/routes/storybook.popover/route.tsx | 6 +++--- apps/webapp/app/routes/storybook.timeline/route.tsx | 2 +- 12 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 4b0d1d4aed9..5b70f2d7707 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -55,6 +55,8 @@ "no-multi-assign": "error", "prefer-object-spread": "error", "react/jsx-no-target-blank": "error", + "react/jsx-fragments": "error", + "react/self-closing-comp": "error", "trigger/no-thrown-unawaited-redirect": "error", "trigger-prisma/no-unbounded-list-filter": "error", "trigger-prisma/no-unbounded-list-filter-in-args-helper": "error" diff --git a/apps/webapp/app/components/code/CodeBlock.tsx b/apps/webapp/app/components/code/CodeBlock.tsx index ee1005eceaf..0dc2cb70311 100644 --- a/apps/webapp/app/components/code/CodeBlock.tsx +++ b/apps/webapp/app/components/code/CodeBlock.tsx @@ -439,7 +439,7 @@ function Chrome({ title }: { title?: string }) {
{title}
-
+
); } diff --git a/apps/webapp/app/components/logs/LogsTable.tsx b/apps/webapp/app/components/logs/LogsTable.tsx index 9034ce906ea..32016a6a7f5 100644 --- a/apps/webapp/app/components/logs/LogsTable.tsx +++ b/apps/webapp/app/components/logs/LogsTable.tsx @@ -220,7 +220,7 @@ export function LogsTable({ } function BlankState({ isLoading, onRefresh }: { isLoading?: boolean; onRefresh?: () => void }) { - if (isLoading) return ; + if (isLoading) return ; const handleRefresh = onRefresh ?? (() => window.location.reload()); diff --git a/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx b/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx index 69bcd3bd117..059f7ffea7d 100644 --- a/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx +++ b/apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx @@ -1,6 +1,6 @@ import { ArrowUpRightIcon } from "@heroicons/react/20/solid"; import { motion } from "framer-motion"; -import { Fragment, useState } from "react"; +import { useState } from "react"; import { BookIcon } from "~/assets/icons/BookIcon"; import { BulbIcon } from "~/assets/icons/BulbIcon"; import { DropdownIcon } from "~/assets/icons/DropdownIcon"; @@ -129,7 +129,7 @@ export function HelpAndFeedback({ sideOffset={isCollapsed ? 8 : 4} align="start" > - + <> {/* This popover lives in the app layout, above both AI hosts, so it opens them through their open-request bridges rather than context. The hosts register the keystrokes; this only shows them. */} @@ -232,7 +232,7 @@ export function HelpAndFeedback({ target="_blank" />
- + {/* Hosted outside the popover so closing the menu can't unmount the form mid-submit. */} diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index ef0c6e25f5e..ddb7220213c 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -717,7 +717,7 @@ function BlankState({ const project = useProject(); const environment = useEnvironment(); const colSpan = showRegion ? 16 : 15; - if (isLoading) return ; + if (isLoading) return ; const { tasks, from, to, ...otherFilters } = filters; const singleTaskFromFilters = filters.tasks.length === 1 ? filters.tasks[0] : null; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx index 310f406e376..48b47499091 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx @@ -831,12 +831,7 @@ function EditEnvironmentVariablePanel({ return ( - +
} - > + /> )} ); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx index 3aca7f06ac2..f045467adfd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx @@ -354,7 +354,7 @@ export default function IntegrationsSettingsPage() { <> {githubAppEnabled && ( - + <> - + )} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index a647468d778..2fffc4bfc62 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -296,7 +296,7 @@ export default function Page() { const result = useTypedLoaderData(); if (!result.foundTask) { - return
; + return
; } const params = useParams(); diff --git a/apps/webapp/app/routes/admin.notifications.tsx b/apps/webapp/app/routes/admin.notifications.tsx index 8e9e4beb374..dbffc75dfbb 100644 --- a/apps/webapp/app/routes/admin.notifications.tsx +++ b/apps/webapp/app/routes/admin.notifications.tsx @@ -453,7 +453,7 @@ export default function AdminNotificationsRoute() { Clicked Dismissed Status - + diff --git a/apps/webapp/app/routes/storybook.popover/route.tsx b/apps/webapp/app/routes/storybook.popover/route.tsx index 529156bc9cd..16c5b6342d0 100644 --- a/apps/webapp/app/routes/storybook.popover/route.tsx +++ b/apps/webapp/app/routes/storybook.popover/route.tsx @@ -1,5 +1,5 @@ import { FolderIcon, PlusIcon } from "@heroicons/react/20/solid"; -import { Fragment, useState } from "react"; +import { useState } from "react"; import { Popover, PopoverArrowTrigger, @@ -19,14 +19,14 @@ export default function Story() { className="min-w-80 overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control" align="start" > - + <>
-
+
diff --git a/apps/webapp/app/routes/storybook.timeline/route.tsx b/apps/webapp/app/routes/storybook.timeline/route.tsx index 6369e61340c..2ee527a065f 100644 --- a/apps/webapp/app/routes/storybook.timeline/route.tsx +++ b/apps/webapp/app/routes/storybook.timeline/route.tsx @@ -156,7 +156,7 @@ export default function Story() { {/* The main body */}
-
+
Date: Wed, 19 Aug 2026 08:28:58 +0100 Subject: [PATCH 21/98] chore: reject redundant standalone blocks (#4675) ## Summary Enable the rule that rejects unnecessary standalone blocks. The existing empty branches are removed so future control flow remains purposeful. Base: [#4674](https://github.com/triggerdotdev/trigger.dev/pull/4674) --- .oxlintrc.json | 1 + packages/cli-v3/src/utilities/initialBanner.ts | 1 - packages/core/src/v3/apiClient/core.ts | 5 +---- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 5b70f2d7707..8e2f5d01ff6 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -57,6 +57,7 @@ "react/jsx-no-target-blank": "error", "react/jsx-fragments": "error", "react/self-closing-comp": "error", + "no-lone-blocks": "error", "trigger/no-thrown-unawaited-redirect": "error", "trigger-prisma/no-unbounded-list-filter": "error", "trigger-prisma/no-unbounded-list-filter-in-args-helper": "error" diff --git a/packages/cli-v3/src/utilities/initialBanner.ts b/packages/cli-v3/src/utilities/initialBanner.ts index 74e217c6e61..d1829be8e5b 100644 --- a/packages/cli-v3/src/utilities/initialBanner.ts +++ b/packages/cli-v3/src/utilities/initialBanner.ts @@ -55,7 +55,6 @@ export async function printInitialBanner(performUpdateCheck = true, profile?: st Run \`npm install --save-dev trigger.dev@${newMajor}\` to update to the latest version. After installation, run Trigger.dev with \`npx trigger.dev\`.` ); - } else { } } else { $spinner.stop("On latest version"); diff --git a/packages/core/src/v3/apiClient/core.ts b/packages/core/src/v3/apiClient/core.ts index 23fb5b36f79..f00cfed66a4 100644 --- a/packages/core/src/v3/apiClient/core.ts +++ b/packages/core/src/v3/apiClient/core.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { fromZodError, ValidationError } from "zod-validation-error"; +import { fromZodError } from "zod-validation-error"; import type { RetryOptions } from "../schemas/index.js"; import { calculateNextRetryDelay } from "../utils/retries.js"; import { ApiConnectionError, ApiError, ApiSchemaValidationError } from "./errors.js"; @@ -274,9 +274,6 @@ async function _doZodFetchWithRetries( throw error; } - if (error instanceof ValidationError) { - } - if (options?.retry) { const retry = { ...defaultRetryOptions, ...options.retry }; From f4320937c5027ba6fac4154ad554d383a6a0ca34 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 08:28:58 +0100 Subject: [PATCH 22/98] chore: prefer direct iteration and function callback types (#4677) ## Summary Enable lint rules that prefer direct iteration and concise function callback types. The existing code now uses direct iteration where no index is needed, and callback contracts use function types consistently. Base: [#4675](https://github.com/triggerdotdev/trigger.dev/pull/4675) --- .oxlintrc.json | 2 ++ .../realtime/redisRealtimeStreams.server.ts | 3 +- .../sanitizeRowsOnParseError.server.ts | 4 +-- .../clickhouse/src/client/client.ts | 4 +-- .../run-engine/src/run-queue/index.ts | 6 ++-- .../schedule-engine/src/engine/types.ts | 12 +++---- .../webhook-engine/src/engine/types.ts | 32 ++++++++----------- .../src/mollifier/drainer.test.ts | 4 +-- packages/trigger-sdk/src/v3/retry.ts | 4 +-- 9 files changed, 32 insertions(+), 39 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 8e2f5d01ff6..a149d115a3b 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -58,6 +58,8 @@ "react/jsx-fragments": "error", "react/self-closing-comp": "error", "no-lone-blocks": "error", + "typescript/prefer-function-type": "error", + "typescript/prefer-for-of": "error", "trigger/no-thrown-unawaited-redirect": "error", "trigger-prisma/no-unbounded-list-filter": "error", "trigger-prisma/no-unbounded-list-filter-in-args-helper": "error" diff --git a/apps/webapp/app/services/realtime/redisRealtimeStreams.server.ts b/apps/webapp/app/services/realtime/redisRealtimeStreams.server.ts index 085ac8e038b..6fbb26e5c9c 100644 --- a/apps/webapp/app/services/realtime/redisRealtimeStreams.server.ts +++ b/apps/webapp/app/services/realtime/redisRealtimeStreams.server.ts @@ -117,8 +117,7 @@ export class RedisRealtimeStreams implements StreamIngestor, StreamResponder { const [_key, entries] = messages[0]; let foundData = false; - for (let i = 0; i < entries.length; i++) { - const [id, fields] = entries[i]; + for (const [id, fields] of entries) { lastId = id; if (fields && fields.length >= 2) { diff --git a/apps/webapp/app/v3/eventRepository/sanitizeRowsOnParseError.server.ts b/apps/webapp/app/v3/eventRepository/sanitizeRowsOnParseError.server.ts index 5f0c67d3b0b..0d6de2e65a0 100644 --- a/apps/webapp/app/v3/eventRepository/sanitizeRowsOnParseError.server.ts +++ b/apps/webapp/app/v3/eventRepository/sanitizeRowsOnParseError.server.ts @@ -172,8 +172,8 @@ export function sanitizeUnknownInPlace(value: unknown): { value: unknown; fixed: export function sanitizeRows(rows: T[]): SanitizeResult { const result: SanitizeResult = { rowsTouched: 0, fieldsSanitized: 0 }; - for (let i = 0; i < rows.length; i++) { - const { fixed } = sanitizeUnknownInPlace(rows[i]); + for (const row of rows) { + const { fixed } = sanitizeUnknownInPlace(row); if (fixed > 0) { result.rowsTouched++; result.fieldsSanitized += fixed; diff --git a/internal-packages/clickhouse/src/client/client.ts b/internal-packages/clickhouse/src/client/client.ts index a61598360b3..9949081d504 100644 --- a/internal-packages/clickhouse/src/client/client.ts +++ b/internal-packages/clickhouse/src/client/client.ts @@ -897,8 +897,8 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter { // Build compact format: [columns, ...rows] const compactData: any[] = [Array.from(req.columns)]; - for (let i = 0; i < eventsArray.length; i++) { - compactData.push(req.toArray(eventsArray[i])); + for (const event of eventsArray) { + compactData.push(req.toArray(event)); } const [clickhouseError, result] = await tryCatch( diff --git a/internal-packages/run-engine/src/run-queue/index.ts b/internal-packages/run-engine/src/run-queue/index.ts index 1edc83380e4..48a84785134 100644 --- a/internal-packages/run-engine/src/run-queue/index.ts +++ b/internal-packages/run-engine/src/run-queue/index.ts @@ -216,9 +216,9 @@ export type RunQueueOptions = { }; }; -interface ConcurrencySweeperCallback { - (runIds: string[]): Promise>; -} +type ConcurrencySweeperCallback = ( + runIds: string[] +) => Promise>; type DequeuedMessage = { messageId: string; diff --git a/internal-packages/schedule-engine/src/engine/types.ts b/internal-packages/schedule-engine/src/engine/types.ts index 4cb72fd2f6e..2cbd8c2d2d8 100644 --- a/internal-packages/schedule-engine/src/engine/types.ts +++ b/internal-packages/schedule-engine/src/engine/types.ts @@ -27,13 +27,11 @@ export type TriggerScheduledTaskParams = { export type TriggerScheduledTaskErrorType = "QUEUE_LIMIT" | "OUT_OF_ENTITLEMENTS" | "SYSTEM_ERROR"; -export interface TriggerScheduledTaskCallback { - (params: TriggerScheduledTaskParams): Promise<{ - success: boolean; - error?: string; - errorType?: TriggerScheduledTaskErrorType; - }>; -} +export type TriggerScheduledTaskCallback = (params: TriggerScheduledTaskParams) => Promise<{ + success: boolean; + error?: string; + errorType?: TriggerScheduledTaskErrorType; +}>; export interface ScheduleEngineOptions { logger?: Logger; diff --git a/internal-packages/webhook-engine/src/engine/types.ts b/internal-packages/webhook-engine/src/engine/types.ts index 2787660915e..15b0f1f8ac7 100644 --- a/internal-packages/webhook-engine/src/engine/types.ts +++ b/internal-packages/webhook-engine/src/engine/types.ts @@ -16,14 +16,12 @@ export type TriggerWebhookTaskParams = { endpointMetadata: unknown; // endpoint.metadata -> run metadata }; -export interface TriggerWebhookTaskCallback { - (params: TriggerWebhookTaskParams): Promise<{ - success: boolean; - runId?: string; // persisted onto WebhookDelivery.runId on success - error?: string; - errorType?: WebhookDeliverTaskErrorType; - }>; -} +export type TriggerWebhookTaskCallback = (params: TriggerWebhookTaskParams) => Promise<{ + success: boolean; + runId?: string; // persisted onto WebhookDelivery.runId on success + error?: string; + errorType?: WebhookDeliverTaskErrorType; +}>; export interface WebhookEngineOptions { logger?: Logger; @@ -82,16 +80,14 @@ export type DeliverWebhookToSessionParams = { isSessionStart: boolean; }; -export interface DeliverWebhookToSessionCallback { - (params: DeliverWebhookToSessionParams): Promise<{ - success: boolean; - runId?: string; // the session's current run, persisted onto WebhookDelivery.runId - error?: string; - errorType?: WebhookDeliverTaskErrorType; - skipped?: boolean; // resume-only and no session existed: recorded FILTERED, not routed - skippedReason?: string; - }>; -} +export type DeliverWebhookToSessionCallback = (params: DeliverWebhookToSessionParams) => Promise<{ + success: boolean; + runId?: string; // the session's current run, persisted onto WebhookDelivery.runId + error?: string; + errorType?: WebhookDeliverTaskErrorType; + skipped?: boolean; // resume-only and no session existed: recorded FILTERED, not routed + skippedReason?: string; +}>; export type IngestInput = { opaqueId: string; // Q2: globally unique, so ingest resolves the endpoint (and its env id + type) from it diff --git a/packages/redis-worker/src/mollifier/drainer.test.ts b/packages/redis-worker/src/mollifier/drainer.test.ts index 6d42be29cb7..b67538a195f 100644 --- a/packages/redis-worker/src/mollifier/drainer.test.ts +++ b/packages/redis-worker/src/mollifier/drainer.test.ts @@ -1310,7 +1310,7 @@ describe("MollifierDrainer per-tick org cap", () => { // Cursor advances by 1 each tick. Over envs.length ticks every env // appears in exactly `sliceSize` of them (slices overlap — intentional, // see the head-of-line fairness test below). - for (let i = 0; i < allEnvs.length; i++) { + for (const _ of allEnvs) { await drainer.runOnce(); } @@ -1356,7 +1356,7 @@ describe("MollifierDrainer per-tick org cap", () => { logger: new Logger("test-drainer", "log"), }); - for (let tick = 0; tick < allEnvs.length; tick++) { + for (const _ of allEnvs) { currentTick = []; await drainer.runOnce(); currentTick.forEach((env, position) => { diff --git a/packages/trigger-sdk/src/v3/retry.ts b/packages/trigger-sdk/src/v3/retry.ts index 110d8d857b6..4277091c4a3 100644 --- a/packages/trigger-sdk/src/v3/retry.ts +++ b/packages/trigger-sdk/src/v3/retry.ts @@ -434,9 +434,7 @@ const getRetryStrategyForResponse = async ( const statusCodes = Object.keys(retry); const clonedResponse = response.clone(); - for (let i = 0; i < statusCodes.length; i++) { - const statusRange = statusCodes[i]; - + for (const statusRange of statusCodes) { if (!statusRange) { continue; } From e0d96c3991127bfb0878e48a9a3a505d0447151a Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 08:28:59 +0100 Subject: [PATCH 23/98] perf(webapp): memoize shared context values (#4678) ## Summary Memoize shared context values so provider renders do not unnecessarily rerender every consumer. Oxlint now enforces this pattern for the rest of the dashboard. Base: [#4677](https://github.com/triggerdotdev/trigger.dev/pull/4677) --- .oxlintrc.json | 10 ++++++ apps/webapp/app/components/SetupCommands.tsx | 11 +++--- .../components/primitives/LocaleProvider.tsx | 4 +-- .../primitives/OperatingSystemProvider.tsx | 6 ++-- .../primitives/SelectedItemsProvider.tsx | 23 +++++------- .../app/components/primitives/Table.tsx | 13 +++++-- .../primitives/charts/DateRangeContext.tsx | 35 ++++++++++--------- 7 files changed, 60 insertions(+), 42 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index a149d115a3b..d59205f7e77 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -57,6 +57,7 @@ "react/jsx-no-target-blank": "error", "react/jsx-fragments": "error", "react/self-closing-comp": "error", + "react/jsx-no-constructed-context-values": "error", "no-lone-blocks": "error", "typescript/prefer-function-type": "error", "typescript/prefer-for-of": "error", @@ -91,6 +92,15 @@ "rules": { "prefer-object-has-own": "off" } + }, + { + "files": [ + "apps/webapp/app/components/primitives/charts/Chart.tsx", + "apps/webapp/app/components/primitives/Timeline.tsx" + ], + "rules": { + "react/jsx-no-constructed-context-values": "off" + } } ] } diff --git a/apps/webapp/app/components/SetupCommands.tsx b/apps/webapp/app/components/SetupCommands.tsx index 54dc2b65293..7d4cc00d0e8 100644 --- a/apps/webapp/app/components/SetupCommands.tsx +++ b/apps/webapp/app/components/SetupCommands.tsx @@ -1,5 +1,5 @@ import { CheckIcon, SparklesIcon } from "@heroicons/react/20/solid"; -import { createContext, useContext, useRef, useState } from "react"; +import { createContext, useContext, useMemo, useRef, useState } from "react"; import { useAppOrigin } from "~/hooks/useAppOrigin"; import { useProject } from "~/hooks/useProject"; import { useTriggerCliTag } from "~/hooks/useTriggerCliTag"; @@ -24,10 +24,13 @@ const PackageManagerContext = createContext ({ activePackageManager, setActivePackageManager }), + [activePackageManager] + ); + return ( - - {children} - + {children} ); } diff --git a/apps/webapp/app/components/primitives/LocaleProvider.tsx b/apps/webapp/app/components/primitives/LocaleProvider.tsx index cb27a03a9f8..c55e6170630 100644 --- a/apps/webapp/app/components/primitives/LocaleProvider.tsx +++ b/apps/webapp/app/components/primitives/LocaleProvider.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { createContext, useContext } from "react"; +import { createContext, useContext, useMemo } from "react"; type LocaleContext = { locales: string[]; @@ -13,7 +13,7 @@ type LocaleContextProviderProps = { const Context = createContext(null); export const LocaleContextProvider = ({ locales, children }: LocaleContextProviderProps) => { - const value = { locales }; + const value = useMemo(() => ({ locales }), [locales]); return {children}; }; diff --git a/apps/webapp/app/components/primitives/OperatingSystemProvider.tsx b/apps/webapp/app/components/primitives/OperatingSystemProvider.tsx index 7f7f8fc57b6..a50ac25d69e 100644 --- a/apps/webapp/app/components/primitives/OperatingSystemProvider.tsx +++ b/apps/webapp/app/components/primitives/OperatingSystemProvider.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { createContext, useContext } from "react"; +import { createContext, useContext, useMemo } from "react"; export type OperatingSystemPlatform = "mac" | "windows"; @@ -18,7 +18,9 @@ export const OperatingSystemContextProvider = ({ platform, children, }: OperatingSystemContextProviderProps) => { - return {children}; + const value = useMemo(() => ({ platform }), [platform]); + + return {children}; }; const throwIfNoProvider = () => { diff --git a/apps/webapp/app/components/primitives/SelectedItemsProvider.tsx b/apps/webapp/app/components/primitives/SelectedItemsProvider.tsx index 12f4b68f9ef..260d0d40bb3 100644 --- a/apps/webapp/app/components/primitives/SelectedItemsProvider.tsx +++ b/apps/webapp/app/components/primitives/SelectedItemsProvider.tsx @@ -1,6 +1,6 @@ "use client"; -import { createContext, useCallback, useContext, useReducer } from "react"; +import { createContext, useCallback, useContext, useMemo, useReducer } from "react"; type SelectedItemsContext = { selectedItems: Set; @@ -60,21 +60,14 @@ export function SelectedItemsProvider({ [state] ); + const contextValue = useMemo( + () => ({ selectedItems: state.items, select, deselect, toggle, deselectAll, has, hasAll }), + [state.items, select, deselect, toggle, deselectAll, has, hasAll] + ); + return ( - - {typeof children === "function" - ? children({ - selectedItems: state.items, - select, - deselect, - toggle, - deselectAll, - has, - hasAll, - }) - : children} + + {typeof children === "function" ? children(contextValue) : children} ); } diff --git a/apps/webapp/app/components/primitives/Table.tsx b/apps/webapp/app/components/primitives/Table.tsx index e0dca744935..bd9b2e82185 100644 --- a/apps/webapp/app/components/primitives/Table.tsx +++ b/apps/webapp/app/components/primitives/Table.tsx @@ -1,7 +1,14 @@ import { ChevronDownIcon, ChevronUpDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid"; import { Link } from "@remix-run/react"; import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react"; -import React, { type ReactNode, createContext, forwardRef, useContext, useState } from "react"; +import React, { + type ReactNode, + createContext, + forwardRef, + useContext, + useMemo, + useState, +} from "react"; import { useCopy } from "~/hooks/useCopy"; import { cn } from "~/utils/cn"; import { Popover, PopoverContent, PopoverVerticalEllipseTrigger } from "./Popover"; @@ -84,8 +91,10 @@ export const Table = forwardRef { + const contextValue = useMemo(() => ({ variant }), [variant]); + return ( - +
(defaultStartISO); const [endDate, setEndDate] = useState(defaultEndISO); - const setDateRange = (start: string, end: string) => { + const setDateRange = useCallback((start: string, end: string) => { setStartDate(start); setEndDate(end); - }; + }, []); - const resetDateRange = () => { + const resetDateRange = useCallback(() => { setStartDate(defaultStartISO); setEndDate(defaultEndISO); - }; - - return ( - - {children} - + }, [defaultEndISO, defaultStartISO]); + + const contextValue = useMemo( + () => ({ startDate, endDate, setDateRange, resetDateRange }), + [startDate, endDate, setDateRange, resetDateRange] ); + + return {children}; } export function useDateRange(): DateRangeContextType | null { From 7fca39c91dd3ff4241d75aeaf96868415e0c11da Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 08:28:59 +0100 Subject: [PATCH 24/98] chore: enable React correctness safeguards (#4679) ## Summary Enable React correctness rules that catch invalid DOM attributes, unsafe legacy APIs, and malformed component contracts before they reach users. Base: [#4678](https://github.com/triggerdotdev/trigger.dev/pull/4678) --- .oxlintrc.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.oxlintrc.json b/.oxlintrc.json index d59205f7e77..e0e7dd84333 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -58,6 +58,22 @@ "react/jsx-fragments": "error", "react/self-closing-comp": "error", "react/jsx-no-constructed-context-values": "error", + "react/no-children-prop": "error", + "react/no-danger-with-children": "error", + "react/no-direct-mutation-state": "error", + "react/no-find-dom-node": "error", + "react/no-is-mounted": "error", + "react/no-render-return-value": "error", + "react/no-string-refs": "error", + "react/no-unsafe": "error", + "react/no-will-update-set-state": "error", + "react/require-render-return": "error", + "react/style-prop-object": "error", + "react/void-dom-elements-no-children": "error", + "react/checked-requires-onchange-or-readonly": "error", + "react/forward-ref-uses-ref": "error", + "react/iframe-missing-sandbox": "error", + "react/no-unknown-property": "error", "no-lone-blocks": "error", "typescript/prefer-function-type": "error", "typescript/prefer-for-of": "error", From c3016eb9e4d79087dbfd15491f63c736b31d1416 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 08:29:00 +0100 Subject: [PATCH 25/98] chore: enable accessibility lint safeguards (#4680) ## Summary Enable accessibility rules that catch invalid ARIA usage, inaccessible media, and invalid focus behavior before they reach users. Base: [#4679](https://github.com/triggerdotdev/trigger.dev/pull/4679) --- .oxlintrc.json | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index e0e7dd84333..92402d316f3 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -3,7 +3,7 @@ "categories": { "correctness": "error" }, - "plugins": ["typescript", "import", "react"], + "plugins": ["typescript", "import", "react", "jsx-a11y"], "jsPlugins": [ "./oxlint-plugins/no-thrown-unawaited-redirect.mjs", "./oxlint-plugins/runops-residency.mjs", @@ -74,6 +74,36 @@ "react/forward-ref-uses-ref": "error", "react/iframe-missing-sandbox": "error", "react/no-unknown-property": "error", + "jsx-a11y/alt-text": "off", + "jsx-a11y/aria-role": "off", + "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/control-has-associated-label": "off", + "jsx-a11y/label-has-associated-control": "off", + "jsx-a11y/no-autofocus": "off", + "jsx-a11y/no-noninteractive-element-interactions": "off", + "jsx-a11y/no-static-element-interactions": "off", + "jsx-a11y/prefer-tag-over-role": "off", + "jsx-a11y/anchor-ambiguous-text": "error", + "jsx-a11y/anchor-has-content": "error", + "jsx-a11y/anchor-is-valid": "error", + "jsx-a11y/aria-activedescendant-has-tabindex": "error", + "jsx-a11y/aria-props": "error", + "jsx-a11y/aria-proptypes": "error", + "jsx-a11y/aria-unsupported-elements": "error", + "jsx-a11y/autocomplete-valid": "error", + "jsx-a11y/heading-has-content": "error", + "jsx-a11y/html-has-lang": "error", + "jsx-a11y/iframe-has-title": "error", + "jsx-a11y/img-redundant-alt": "error", + "jsx-a11y/media-has-caption": "error", + "jsx-a11y/no-access-key": "error", + "jsx-a11y/no-aria-hidden-on-focusable": "error", + "jsx-a11y/no-distracting-elements": "error", + "jsx-a11y/no-redundant-roles": "error", + "jsx-a11y/role-has-required-aria-props": "error", + "jsx-a11y/role-supports-aria-props": "error", + "jsx-a11y/scope": "error", + "jsx-a11y/tabindex-no-positive": "error", "no-lone-blocks": "error", "typescript/prefer-function-type": "error", "typescript/prefer-for-of": "error", From 1aeb356b9e67a2a753a9f8f78313cb8000cb5ce7 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 08:29:00 +0100 Subject: [PATCH 26/98] fix(webapp): preserve React hook order (#4681) ## Summary Call dashboard hooks unconditionally so components keep a stable hook order when their props change. Base: [#4680](https://github.com/triggerdotdev/trigger.dev/pull/4680) --- apps/webapp/app/components/SetupCommands.tsx | 4 ++-- .../webapp/app/components/primitives/Tabs.tsx | 20 +++++++++---------- apps/webapp/app/hooks/useTypedMatchData.ts | 6 ++---- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/apps/webapp/app/components/SetupCommands.tsx b/apps/webapp/app/components/SetupCommands.tsx index 7d4cc00d0e8..1e9cda1255e 100644 --- a/apps/webapp/app/components/SetupCommands.tsx +++ b/apps/webapp/app/components/SetupCommands.tsx @@ -57,7 +57,7 @@ function useApiUrl() { } } -function getApiUrlArg() { +function useApiUrlArg() { const apiUrl = useApiUrl(); return apiUrl ? `-a ${apiUrl}` : undefined; } @@ -70,7 +70,7 @@ type TabsProps = { export function InitCommandV3({ title }: TabsProps) { const project = useProject(); const projectRef = project.externalRef; - const apiUrlArg = getApiUrlArg(); + const apiUrlArg = useApiUrlArg(); const triggerCliTag = useTriggerCliTag(); const initCommandParts = [`trigger.dev@${triggerCliTag}`, "init", `-p ${projectRef}`, apiUrlArg]; diff --git a/apps/webapp/app/components/primitives/Tabs.tsx b/apps/webapp/app/components/primitives/Tabs.tsx index 3df60f92b25..b1284944d15 100644 --- a/apps/webapp/app/components/primitives/Tabs.tsx +++ b/apps/webapp/app/components/primitives/Tabs.tsx @@ -234,17 +234,15 @@ export function TabButton({ } & React.ButtonHTMLAttributes) { const ref = useRef(null); - if (shortcut) { - useShortcutKeys({ - shortcut: shortcut, - action: () => { - if (ref.current) { - ref.current.click(); - } - }, - disabled: props.disabled, - }); - } + useShortcutKeys({ + shortcut, + action: () => { + if (ref.current) { + ref.current.click(); + } + }, + disabled: props.disabled, + }); const title = variant === "title"; diff --git a/apps/webapp/app/hooks/useTypedMatchData.ts b/apps/webapp/app/hooks/useTypedMatchData.ts index 6022fee7550..056e5116b87 100644 --- a/apps/webapp/app/hooks/useTypedMatchData.ts +++ b/apps/webapp/app/hooks/useTypedMatchData.ts @@ -23,11 +23,9 @@ export function useTypedMatchesData({ id: string; matches?: UIMatch[]; }): UseDataFunctionReturn | undefined { - if (!matches) { - matches = useMatches(); - } + const routeMatches = useMatches(); - return useTypedDataFromMatches({ id, matches }); + return useTypedDataFromMatches({ id, matches: matches ?? routeMatches }); } function useTypedMatchData( From 219bc09d5f9e42e1d0a0e33416f944ec90ef57d5 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 08:29:01 +0100 Subject: [PATCH 27/98] perf(webapp): stabilize chart loading line renderer (#4682) ## Summary Keep the chart loading line renderer stable across parent renders so its animated SVG paths retain their component identity. Base: [#4681](https://github.com/triggerdotdev/trigger.dev/pull/4681) --- .../primitives/charts/ChartLoading.tsx | 103 ++++++++---------- 1 file changed, 48 insertions(+), 55 deletions(-) diff --git a/apps/webapp/app/components/primitives/charts/ChartLoading.tsx b/apps/webapp/app/components/primitives/charts/ChartLoading.tsx index 8e7ee031f81..6862970b1f7 100644 --- a/apps/webapp/app/components/primitives/charts/ChartLoading.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartLoading.tsx @@ -187,72 +187,60 @@ function ChartBarLoadingBackground() { ); } -function ChartLineLoadingBackground() { - // Generate line points with configurable starting position and constraints - const generateLinePoints = (startY: number, minY: number, maxY: number) => { - const numPoints = 10; - const points = []; - let lastY = startY; - - for (let i = 0; i < numPoints; i++) { - // Calculate x value that spreads points across the full width - const x = i * (9 / (numPoints - 1)); +type ChartPoint = { x: number; y: number }; - // Create less extreme variations that move smoothly - const change = Math.random() * 6 - 3; // Range from -3 to +3 - const y = Math.max(minY, Math.min(maxY, lastY + change)); // Apply constraints +function generateLinePoints(startY: number, minY: number, maxY: number): ChartPoint[] { + const numPoints = 10; + const points = []; + let lastY = startY; - points.push({ x, y }); - lastY = y; - } + for (let i = 0; i < numPoints; i++) { + const x = i * (9 / (numPoints - 1)); + const change = Math.random() * 6 - 3; + const y = Math.max(minY, Math.min(maxY, lastY + change)); - return points; - }; + points.push({ x, y }); + lastY = y; + } - // Generate points for both lines - const points = useMemo(() => generateLinePoints(30, 10, 90), []); - const secondPoints = useMemo(() => generateLinePoints(40, 30, 90), []); - - const generateSmoothPath = (points: Array<{ x: number; y: number }>) => { - if (points.length < 2) return ""; + return points; +} - let path = `M0,${50 - points[0].y}`; +function generateSmoothPath(points: ChartPoint[]) { + if (points.length < 2) return ""; - // Use curve command for smooth lines - for (let i = 0; i < points.length - 1; i++) { - const x1 = points[i].x; - const y1 = 50 - points[i].y; - const x2 = points[i + 1].x; - const y2 = 50 - points[i + 1].y; + let path = `M0,${50 - points[0].y}`; - // Bezier control points (create smooth curve) - const cx1 = (x1 + x2) / 2; - const cy1 = y1; - const cx2 = (x1 + x2) / 2; - const cy2 = y2; + for (let i = 0; i < points.length - 1; i++) { + const x1 = points[i].x; + const y1 = 50 - points[i].y; + const x2 = points[i + 1].x; + const y2 = 50 - points[i + 1].y; + const cx1 = (x1 + x2) / 2; + const cy1 = y1; + const cx2 = (x1 + x2) / 2; + const cy2 = y2; - path += ` C${cx1},${cy1} ${cx2},${cy2} ${x2},${y2}`; - } + path += ` C${cx1},${cy1} ${cx2},${cy2} ${x2},${y2}`; + } - return path; - }; + return path; +} - const generateAreaPath = (points: Array<{ x: number; y: number }>) => { - const curvePath = generateSmoothPath(points); - const lastX = 9; - return `${curvePath} L${lastX},50 L0,50 Z`; - }; +function generateAreaPath(points: ChartPoint[]) { + return `${generateSmoothPath(points)} L9,50 L0,50 Z`; +} - // Component to render a line with area fill and animation - const AnimatedLine = ({ - points, - gradientId, - delay = 0, - }: { - points: Array<{ x: number; y: number }>; - gradientId: string; - delay?: number; - }) => ( +function AnimatedLine({ + points, + gradientId, + delay = 0, +}: { + points: ChartPoint[]; + gradientId: string; + delay?: number; +}) { + return ( <> ); +} + +function ChartLineLoadingBackground() { + const points = useMemo(() => generateLinePoints(30, 10, 90), []); + const secondPoints = useMemo(() => generateLinePoints(40, 30, 90), []); return ( Date: Wed, 19 Aug 2026 08:29:01 +0100 Subject: [PATCH 28/98] refactor(webapp): remove redundant React fragments (#4683) ## Summary Remove redundant React fragments from dashboard components, leaving their rendered output unchanged while simplifying component trees. Base: [#4682](https://github.com/triggerdotdev/trigger.dev/pull/4682) --- .../app/components/primitives/FormError.tsx | 30 ++-- .../webapp/app/components/primitives/Icon.tsx | 2 +- .../app/components/query/QueryEditor.tsx | 130 ++++++++-------- .../webapp/app/components/run/RunTimeline.tsx | 140 +++++++++--------- .../components/runs/v3/WaitpointDetails.tsx | 9 +- .../route.tsx | 30 ++-- .../app/routes/storybook.select/route.tsx | 24 ++- 7 files changed, 174 insertions(+), 191 deletions(-) diff --git a/apps/webapp/app/components/primitives/FormError.tsx b/apps/webapp/app/components/primitives/FormError.tsx index 2f8de556e12..e9793158752 100644 --- a/apps/webapp/app/components/primitives/FormError.tsx +++ b/apps/webapp/app/components/primitives/FormError.tsx @@ -12,21 +12,17 @@ export function FormError({ id?: string; className?: string; }) { - return ( - <> - {children && ( - - - - {children} - - - )} - - ); + return children ? ( + + + + {children} + + + ) : null; } diff --git a/apps/webapp/app/components/primitives/Icon.tsx b/apps/webapp/app/components/primitives/Icon.tsx index 4b8197f93ce..2f2ff761240 100644 --- a/apps/webapp/app/components/primitives/Icon.tsx +++ b/apps/webapp/app/components/primitives/Icon.tsx @@ -18,7 +18,7 @@ export function Icon(props: IconProps) { } if (React.isValidElement(props.icon)) { - return <>{props.icon}; + return props.icon; } if ( diff --git a/apps/webapp/app/components/query/QueryEditor.tsx b/apps/webapp/app/components/query/QueryEditor.tsx index d46093929fe..ea41be33772 100644 --- a/apps/webapp/app/components/query/QueryEditor.tsx +++ b/apps/webapp/app/components/query/QueryEditor.tsx @@ -1187,38 +1187,36 @@ function ResultsChart({ accessory?: ReactNode; }) { return ( - <> - - -
- - } - query={query} - data={{ - rows, - columns, - }} - config={{ - type: "chart", - ...chartConfig, - }} - accessory={accessory} - /> -
-
- - - - -
- + + +
+ + } + query={query} + data={{ + rows, + columns, + }} + config={{ + type: "chart", + ...chartConfig, + }} + accessory={accessory} + /> +
+
+ + + + +
); } @@ -1266,42 +1264,40 @@ function ResultsBigNumber({ }, [columns]); return ( - <> - - -
- - } - query={query} - data={{ - rows, - columns, - }} - config={{ - type: "bignumber", - ...bigNumberConfig, - }} - accessory={accessory} - /> -
-
- - - + +
+ + } + query={query} + data={{ + rows, + columns, + }} + config={{ + type: "bignumber", + ...bigNumberConfig, + }} + accessory={accessory} /> - - - +
+
+ + + + +
); } diff --git a/apps/webapp/app/components/run/RunTimeline.tsx b/apps/webapp/app/components/run/RunTimeline.tsx index a6f024ac05f..6fabadaf8a1 100644 --- a/apps/webapp/app/components/run/RunTimeline.tsx +++ b/apps/webapp/app/components/run/RunTimeline.tsx @@ -594,86 +594,84 @@ export function SpanTimeline({ const visibleEvents = events ?? []; return ( - <> -
- {visibleEvents.map((event, index) => { - // Store previous date to compare - const prevDate = index === 0 ? null : visibleEvents[index - 1].timestamp; +
+ {visibleEvents.map((event, index) => { + // Store previous date to compare + const prevDate = index === 0 ? null : visibleEvents[index - 1].timestamp; - return ( - - } - variant={event.markerVariant} - state={state} - helpText={event.helpText} - style={style} - /> - - - ); - })} - 0 ? visibleEvents[visibleEvents.length - 1].timestamp : null + return ( + + } + variant={event.markerVariant} + state={state} + helpText={event.helpText} + style={style} + /> + - } - variant={"start-cap-thick"} + + ); + })} + 0 ? visibleEvents[visibleEvents.length - 1].timestamp : null + } + /> + } + variant={"start-cap-thick"} + state={state} + helpText={getHelpTextForEvent("Started")} + style={style} + /> + {state === "inprogress" ? ( + } state={state} - helpText={getHelpTextForEvent("Started")} + variant="normal" style={style} /> - {state === "inprogress" ? ( + ) : ( + <> } - state={state} + title={formatDuration( + startTime, + new Date(startTime.getTime() + nanosecondsToMilliseconds(duration)) + )} + state={isError ? "error" : undefined} variant="normal" style={style} /> - ) : ( - <> - - - } - state={isError ? "error" : undefined} - variant="end-cap-thick" - helpText={getHelpTextForEvent("Finished")} - style={style} - /> - - )} -
- + + } + state={isError ? "error" : undefined} + variant="end-cap-thick" + helpText={getHelpTextForEvent("Finished")} + style={style} + /> + + )} +
); } diff --git a/apps/webapp/app/components/runs/v3/WaitpointDetails.tsx b/apps/webapp/app/components/runs/v3/WaitpointDetails.tsx index 7841f5248d9..19063131e4a 100644 --- a/apps/webapp/app/components/runs/v3/WaitpointDetails.tsx +++ b/apps/webapp/app/components/runs/v3/WaitpointDetails.tsx @@ -87,9 +87,7 @@ export function WaitpointDetailTable({
{waitpoint.completedAfter ? ( - <> - - + ) : ( "–" )} @@ -127,9 +125,8 @@ export function WaitpointDetailTable({ {waitpoint.completedAt ? : "–"} - {waitpoint.status === "WAITING" ? null : waitpoint.status === "TIMED_OUT" ? ( - <> - ) : waitpoint.output ? ( + {waitpoint.status === "WAITING" ? null : waitpoint.status === + "TIMED_OUT" ? null : waitpoint.output ? ( ) : waitpoint.completedAfter ? null : ( "Completed with no output" diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx index 3f4be602aa7..f56e96cb0e1 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx @@ -18,22 +18,20 @@ export default function Project() { const isImpersonating = useIsImpersonating(); return ( - <> -
- - - - - - -
- +
+ + + + + + +
); } diff --git a/apps/webapp/app/routes/storybook.select/route.tsx b/apps/webapp/app/routes/storybook.select/route.tsx index 27a442af2d6..c9ce1495c96 100644 --- a/apps/webapp/app/routes/storybook.select/route.tsx +++ b/apps/webapp/app/routes/storybook.select/route.tsx @@ -160,19 +160,17 @@ function Statuses() { filter={(item, search) => item.title.toLowerCase().includes(search.toLowerCase())} shortcut={{ key: "s" }} > - {(matches, { shortcutsEnabled }) => ( - <> - {matches?.map((item, index) => ( - - - - ))} - - )} + {(matches, { shortcutsEnabled }) => + matches?.map((item, index) => ( + + + + )) + } ); } From 9de90f7bed49a85e24deb33ae0963cb76a5d1d44 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 19 Aug 2026 08:40:27 +0100 Subject: [PATCH 29/98] ci: pre-pull testcontainer images on fork PRs (#4684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What The `Pre-pull testcontainer images` step is gated on `env.DOCKERHUB_USERNAME`. Fork PRs receive no repository secrets, so that variable is empty and the step is skipped along with the DockerHub login it was grouped with. ## Why With the pre-pull skipped, testcontainers pulls images lazily — inside the first test that resolves the fixture, against that test's `testTimeout`. On PR #4534 that pushed five webapp shards past their 60s cap across three runs, each failing as `Test timed out in 60000ms` while 42 of 43 files in the shard passed. Measured cost of the missing pre-pull, comparing the delta from vitest start to the first container fixture on the same runner class: | Run | Delta | | --- | --- | | internal x2 | +139.9s, +139.4s | | fork x2 | +149.7s, +149.4s | A 10.0s penalty, bimodal to within 0.3s. Note the pulls themselves succeed anonymously — there are no rate-limit errors in any of the failing logs. Only the login needs credentials, so the pre-pull can run unconditionally. ## Scope Removes the `if:` from the pre-pull step in all five workflows that have one. The DockerHub login stays gated, since it genuinely needs secrets. --- .github/workflows/e2e-webapp-auth-full.yml | 1 - .github/workflows/e2e-webapp.yml | 1 - .github/workflows/unit-tests-internal.yml | 1 - .github/workflows/unit-tests-packages.yml | 1 - .github/workflows/unit-tests-webapp.yml | 1 - 5 files changed, 5 deletions(-) diff --git a/.github/workflows/e2e-webapp-auth-full.yml b/.github/workflows/e2e-webapp-auth-full.yml index e88429a3443..96e5a437cbf 100644 --- a/.github/workflows/e2e-webapp-auth-full.yml +++ b/.github/workflows/e2e-webapp-auth-full.yml @@ -99,7 +99,6 @@ jobs: run: echo "DockerHub login skipped because secrets are not available." - name: 🐳 Pre-pull testcontainer images - if: ${{ env.DOCKERHUB_USERNAME }} run: | docker pull postgres:14 docker pull redis:7.2 diff --git a/.github/workflows/e2e-webapp.yml b/.github/workflows/e2e-webapp.yml index 1c1525f31bc..d6b4ff99684 100644 --- a/.github/workflows/e2e-webapp.yml +++ b/.github/workflows/e2e-webapp.yml @@ -74,7 +74,6 @@ jobs: run: echo "DockerHub login skipped because secrets are not available." - name: 🐳 Pre-pull testcontainer images - if: ${{ env.DOCKERHUB_USERNAME }} run: | echo "Pre-pulling Docker images with authenticated session..." docker pull postgres:14 diff --git a/.github/workflows/unit-tests-internal.yml b/.github/workflows/unit-tests-internal.yml index 1a398af9c44..78070b0c14f 100644 --- a/.github/workflows/unit-tests-internal.yml +++ b/.github/workflows/unit-tests-internal.yml @@ -78,7 +78,6 @@ jobs: run: echo "DockerHub login skipped because secrets are not available." - name: 🐳 Pre-pull testcontainer images - if: ${{ env.DOCKERHUB_USERNAME }} run: | # Retry each pull - DockerHub registry timeouts are a recurring transient CI flake. pull() { diff --git a/.github/workflows/unit-tests-packages.yml b/.github/workflows/unit-tests-packages.yml index ceb0b7b49da..38e05fe70aa 100644 --- a/.github/workflows/unit-tests-packages.yml +++ b/.github/workflows/unit-tests-packages.yml @@ -81,7 +81,6 @@ jobs: run: echo "DockerHub login skipped because secrets are not available." - name: 🐳 Pre-pull testcontainer images - if: ${{ env.DOCKERHUB_USERNAME }} run: | # Retry each pull - DockerHub registry timeouts are a recurring transient CI flake. pull() { diff --git a/.github/workflows/unit-tests-webapp.yml b/.github/workflows/unit-tests-webapp.yml index 680eafd9aa1..384959e9cf9 100644 --- a/.github/workflows/unit-tests-webapp.yml +++ b/.github/workflows/unit-tests-webapp.yml @@ -86,7 +86,6 @@ jobs: run: echo "DockerHub login skipped because secrets are not available." - name: 🐳 Pre-pull testcontainer images - if: ${{ env.DOCKERHUB_USERNAME }} run: | # Retry each pull - DockerHub registry timeouts are a recurring transient CI flake. pull() { From 7529c33a5e0f6c44d7a2882ad6a78f459bb5cd3c Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 19 Aug 2026 08:40:28 +0100 Subject: [PATCH 30/98] ci: correct testcontainer pre-pull image lists (#4685) ## What Three corrections to the pre-pull lists, each verified against what the suites actually use. ## Changes **`ryuk:0.11.0` -> `0.14.0`** in `e2e-webapp.yml` and `e2e-webapp-auth-full.yml`. The installed testcontainers hardcodes the image it starts: ```js // testcontainers@11.14.0 build/reaper/reaper.js : ImageName.fromString("testcontainers/ryuk:0.14.0").string; ``` So those two lines were pre-pulling an image nothing starts, and the one actually used was never pre-pulled. The other three workflows already say 0.14.0. **`postgres:17` added** to `unit-tests-webapp.yml`. The webapp suite references `docker.io/postgres:17` across 10 files but only `postgres:14` was pre-pulled. `unit-tests-internal.yml` already pulls both. **Electric pinned to its digest** in `unit-tests-webapp.yml`. The tests run `electricsql/electric:1.2.4@sha256:20da...` while the pre-pull asked for the bare tag, so the pre-pull did not necessarily populate the manifest the tests then request. ## Not changed The otel collector and s2 images are pulled by other workflows but are not used by the webapp suite, so they are deliberately not added here. `postgresAndRedisTest` uses per-test containers by design and needs nothing pre-pulled. --- .github/workflows/e2e-webapp-auth-full.yml | 2 +- .github/workflows/e2e-webapp.yml | 2 +- .github/workflows/unit-tests-internal.yml | 1 - .github/workflows/unit-tests-packages.yml | 1 - .github/workflows/unit-tests-webapp.yml | 3 ++- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e-webapp-auth-full.yml b/.github/workflows/e2e-webapp-auth-full.yml index 96e5a437cbf..3b1c510107b 100644 --- a/.github/workflows/e2e-webapp-auth-full.yml +++ b/.github/workflows/e2e-webapp-auth-full.yml @@ -102,7 +102,7 @@ jobs: run: | docker pull postgres:14 docker pull redis:7.2 - docker pull testcontainers/ryuk:0.11.0 + docker pull testcontainers/ryuk:0.14.0 - name: 📥 Download deps run: pnpm install --frozen-lockfile diff --git a/.github/workflows/e2e-webapp.yml b/.github/workflows/e2e-webapp.yml index d6b4ff99684..a05b4525d12 100644 --- a/.github/workflows/e2e-webapp.yml +++ b/.github/workflows/e2e-webapp.yml @@ -78,7 +78,7 @@ jobs: echo "Pre-pulling Docker images with authenticated session..." docker pull postgres:14 docker pull redis:7.2 - docker pull testcontainers/ryuk:0.11.0 + docker pull testcontainers/ryuk:0.14.0 docker pull ghcr.io/s2-streamstore/s2:0.40.0@sha256:b26249e2ede0949755f5af8028185dc2bcfc3aa2db21eb9610543d144eb6ee9d docker pull minio/minio:latest echo "Image pre-pull complete" diff --git a/.github/workflows/unit-tests-internal.yml b/.github/workflows/unit-tests-internal.yml index 78070b0c14f..9d606bb98fa 100644 --- a/.github/workflows/unit-tests-internal.yml +++ b/.github/workflows/unit-tests-internal.yml @@ -95,7 +95,6 @@ jobs: pull clickhouse/clickhouse-server:26.2.19.43-alpine@sha256:c6ad6a7eb2fb5999df3adfb8b69a0c7222c68fa9b8f6b04a088564ebbc959251 pull redis:7.2 pull testcontainers/ryuk:0.14.0 - pull electricsql/electric:1.2.4 echo "Image pre-pull complete" - name: 📥 Download deps diff --git a/.github/workflows/unit-tests-packages.yml b/.github/workflows/unit-tests-packages.yml index 38e05fe70aa..ebca33d50b2 100644 --- a/.github/workflows/unit-tests-packages.yml +++ b/.github/workflows/unit-tests-packages.yml @@ -97,7 +97,6 @@ jobs: pull clickhouse/clickhouse-server:26.2.19.43-alpine@sha256:c6ad6a7eb2fb5999df3adfb8b69a0c7222c68fa9b8f6b04a088564ebbc959251 pull redis:7.2 pull testcontainers/ryuk:0.14.0 - pull electricsql/electric:1.2.4 pull otel/opentelemetry-collector-k8s:0.158.0@sha256:c09130a633196a5becee164411473a0932ecf223f94fda6dab5f22798ff9f376 echo "Image pre-pull complete" diff --git a/.github/workflows/unit-tests-webapp.yml b/.github/workflows/unit-tests-webapp.yml index 384959e9cf9..bec77bbbc4b 100644 --- a/.github/workflows/unit-tests-webapp.yml +++ b/.github/workflows/unit-tests-webapp.yml @@ -99,10 +99,11 @@ jobs: } echo "Pre-pulling Docker images with authenticated session..." pull postgres:14 + pull postgres:17 pull clickhouse/clickhouse-server:26.2.19.43-alpine@sha256:c6ad6a7eb2fb5999df3adfb8b69a0c7222c68fa9b8f6b04a088564ebbc959251 pull redis:7.2 pull testcontainers/ryuk:0.14.0 - pull electricsql/electric:1.2.4 + pull electricsql/electric:1.2.4@sha256:20da3d0b0e74926c5623392db67fd56698b9e374c4aeb6cb5cadeb8fea171c36 pull minio/minio:latest echo "Image pre-pull complete" From b93904526c6f7181c9381d3483d135d915c61aa6 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 19 Aug 2026 08:40:28 +0100 Subject: [PATCH 31/98] test(testcontainers): hoist container boot off the test timer (#4686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What The one-off worker container boot is billed to whichever test resolves the fixture first. This moves it into a `beforeAll` with its own timeout. ## Why vitest runs the fixture chain *inside* the test timer: ```js // @vitest/runner 4.1.7 setFn(task, withTimeout(...withFixtures(handler)..., timeout, ...)) ``` There is no `fixtureTimeout`. So booting Postgres (plus `CREATE DATABASE`, schema push, ClickHouse and Redis) lands on the first test and consumes a budget sized for test work. That is why losing the image pre-pull on fork PRs was fatal rather than merely slower: the extra ~10s crossed the 60s cap. Since fork time is roughly internal + 10s and forks exceed 60s, internal runs were already clearing that cap by under 10s — a latent flake regardless of forks. ## How `withWarmup` wraps each fixture family and lazily registers a `beforeAll` on first touch, with its own generous timeout. Registration is lazy so only files that actually use a family pay for it — `@internal/testcontainers` is imported by hundreds of test files, many of which only need Redis. It registers once per file, since `isolate` gives each file a fresh module registry. Eight families are wrapped. `isolatedRedisTest`, `replicationContainerTest` and `postgresAndRedisTest` are deliberately untouched: they use per-test containers by design, so there is no one-off boot to hoist. No test file or CI changes, and it applies to every package using these fixtures. ## Verification Proven by mutation. `src/warmup.test.ts` runs container tests under a deliberately tight cap: | | Result | | --- | --- | | with the warm-up | passes | | warm-up neutered | fails, `Test timed out` | It is kept as a regression test — without it, unwrapping a fixture would break nothing visibly. `triggerFailedTask.call.test.ts`, one of the five shard casualties, passes locally in 20.4s. ## Also here `@internal/testcontainers` had no `test` script, so `turbo run test --filter "@internal/*"` skipped the package and its existing `heteroDedicated.test.ts` never ran in CI. Adding the script (matching the sibling packages') runs both files; verified green through turbo exactly as CI invokes it. --- internal-packages/testcontainers/package.json | 3 +- internal-packages/testcontainers/src/index.ts | 170 +++++++++++++----- .../testcontainers/src/warmup.test.ts | 33 ++++ 3 files changed, 156 insertions(+), 50 deletions(-) create mode 100644 internal-packages/testcontainers/src/warmup.test.ts diff --git a/internal-packages/testcontainers/package.json b/internal-packages/testcontainers/package.json index c1e68946aa6..b42f05863bc 100644 --- a/internal-packages/testcontainers/package.json +++ b/internal-packages/testcontainers/package.json @@ -26,6 +26,7 @@ "tinyexec": "^0.3.0" }, "scripts": { - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest --sequence.concurrent=false --no-file-parallelism" } } diff --git a/internal-packages/testcontainers/src/index.ts b/internal-packages/testcontainers/src/index.ts index cceadf6cb45..820ca827aec 100644 --- a/internal-packages/testcontainers/src/index.ts +++ b/internal-packages/testcontainers/src/index.ts @@ -314,10 +314,44 @@ const prismaFromContainer = async ( } }; -export const postgresTest = test.extend({ - postgresContainer: clonedPostgresContainer, - prisma: prismaFromContainer, -}); +const CONTAINER_WARMUP_TIMEOUT_MS = 300_000; + +type WarmableTestApi = { + beforeAll: (fn: (context: any) => Promise, timeout?: number) => void; +}; + +const withWarmup = ( + api: T, + warmUp: (context: any) => Promise +): T => { + const register = () => { + api.beforeAll(warmUp, CONTAINER_WARMUP_TIMEOUT_MS); + }; + + return new Proxy(api, { + apply(target, thisArg, args) { + register(); + return Reflect.apply(target as unknown as (...a: unknown[]) => unknown, thisArg, args); + }, + get(target, prop, receiver) { + if (prop !== "then") { + // awaiting the module is not use + register(); + } + return Reflect.get(target, prop, receiver); + }, + }) as T; +}; + +export const postgresTest = withWarmup( + test.extend({ + postgresContainer: clonedPostgresContainer, + prisma: prismaFromContainer, + }), + async () => { + await getWorkerPostgresContainer(); + } +); type HeteroPostgresTestContext = { // PG14 (legacy / control-plane DB analog) @@ -609,11 +643,16 @@ type RedisTestContext = { // Worker-scoped redis (boots once, FLUSHALL between tests). Use isolatedRedisTest for tests that run // background redis work (redis-worker Workers, BatchQueue) past the test body - see its note + README. -export const redisTest = test.extend({ - redisContainer: [bootWorkerRedis, { scope: "worker" }], - resetRedis: [flushRedis, { auto: true }], - redisOptions, -}); +export const redisTest = withWarmup( + test.extend({ + redisContainer: [bootWorkerRedis, { scope: "worker" }], + resetRedis: [flushRedis, { auto: true }], + redisOptions, + }), + async ({ redisContainer }) => { + void redisContainer; + } +); // Per-test redis for tests with background redis work (redis-worker Workers, BatchQueue) that can // outlive the test body - a shared redis would let leaked work hit a closed connection / next test @@ -723,11 +762,16 @@ const scopedClickhouseClient = async ( } }; -export const clickhouseTest = test.extend({ - clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }], - resetClickhouse: [truncateClickhouseFixture, { auto: true }], - clickhouseClient: scopedClickhouseClient, -}); +export const clickhouseTest = withWarmup( + test.extend({ + clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }], + resetClickhouse: [truncateClickhouseFixture, { auto: true }], + clickhouseClient: scopedClickhouseClient, + }), + async ({ clickhouseContainer }) => { + void clickhouseContainer; + } +); // NOTE: per-test containers (not worker-scoped) - the replication package does logical replication // (slots/publications/REPLICA IDENTITY), which doesn't play nicely with a shared container + @@ -755,17 +799,24 @@ type ContainerTestContext = { // The workhorse fixture (~36 files). Postgres (template-clone), Redis (FLUSHALL) and ClickHouse // (truncate) all boot once per worker - no per-test container boots. Use containerTestWithIsolatedRedis // for tests that run background redis work (BatchQueue, redis-worker Workers) past the test body. -export const containerTest = test.extend({ - postgresContainer: clonedPostgresContainer, - prisma: prismaFromContainer, - schemaOnlyPrisma: schemaOnlyPrismaFixture, - redisContainer: [bootWorkerRedis, { scope: "worker" }], - resetRedis: [flushRedis, { auto: true }], - redisOptions, - clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }], - resetClickhouse: [truncateClickhouseFixture, { auto: true }], - clickhouseClient: scopedClickhouseClient, -}); +export const containerTest = withWarmup( + test.extend({ + postgresContainer: clonedPostgresContainer, + prisma: prismaFromContainer, + schemaOnlyPrisma: schemaOnlyPrismaFixture, + redisContainer: [bootWorkerRedis, { scope: "worker" }], + resetRedis: [flushRedis, { auto: true }], + redisOptions, + clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }], + resetClickhouse: [truncateClickhouseFixture, { auto: true }], + clickhouseClient: scopedClickhouseClient, + }), + async ({ redisContainer, clickhouseContainer }) => { + void redisContainer; + void clickhouseContainer; + await getWorkerPostgresContainer(); + } +); type ContainerWithIsolatedRedisContext = { network: StartedNetwork; @@ -780,16 +831,22 @@ type ContainerWithIsolatedRedisContext = { // Same as containerTest but Redis is PER-TEST - for tests whose background redis work (BatchQueue, // Workers) outlives the test body and would otherwise hit a closed/shared connection. -export const containerTestWithIsolatedRedis = test.extend({ - network, - postgresContainer: clonedPostgresContainer, - prisma: prismaFromContainer, - redisContainer, - redisOptions, - clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }], - resetClickhouse: [truncateClickhouseFixture, { auto: true }], - clickhouseClient: scopedClickhouseClient, -}); +export const containerTestWithIsolatedRedis = withWarmup( + test.extend({ + network, + postgresContainer: clonedPostgresContainer, + prisma: prismaFromContainer, + redisContainer, + redisOptions, + clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }], + resetClickhouse: [truncateClickhouseFixture, { auto: true }], + clickhouseClient: scopedClickhouseClient, + }), + async ({ clickhouseContainer }) => { + void clickhouseContainer; + await getWorkerPostgresContainer(); + } +); type ContainerWithIsolatedRedisNoClickhouseContext = { network: StartedNetwork; @@ -801,14 +858,18 @@ type ContainerWithIsolatedRedisNoClickhouseContext = { // Like containerTestWithIsolatedRedis (template-clone Postgres + per-test Redis) but with no // ClickHouse - for suites that touch Postgres + Redis but never ClickHouse, avoiding its boot+migrate. -export const containerTestWithIsolatedRedisNoClickhouse = +export const containerTestWithIsolatedRedisNoClickhouse = withWarmup( test.extend({ network, postgresContainer: clonedPostgresContainer, prisma: prismaFromContainer, redisContainer, redisOptions, - }); + }), + async () => { + await getWorkerPostgresContainer(); + } +); // For tests that exercise the Postgres -> ClickHouse logical-replication pipeline (WAL slots, // publications, REPLICA IDENTITY). These need a dedicated Postgres per test - the worker-scoped + @@ -887,11 +948,16 @@ type MinioTestContext = { minioConfig: MinIOConnectionConfig; }; -export const minioTest = test.extend({ - minioContainer: [bootWorkerMinio, { scope: "worker" }], - resetMinio: [minioReset, { auto: true }], - minioConfig, -}); +export const minioTest = withWarmup( + test.extend({ + minioContainer: [bootWorkerMinio, { scope: "worker" }], + resetMinio: [minioReset, { auto: true }], + minioConfig, + }), + async ({ minioContainer }) => { + void minioContainer; + } +); type PostgresAndMinioTestContext = { postgresContainer: StartedPostgreSqlContainer; @@ -901,10 +967,16 @@ type PostgresAndMinioTestContext = { minioConfig: MinIOConnectionConfig; }; -export const postgresAndMinioTest = test.extend({ - postgresContainer: clonedPostgresContainer, - prisma: prismaFromContainer, - minioContainer: [bootWorkerMinio, { scope: "worker" }], - resetMinio: [minioReset, { auto: true }], - minioConfig, -}); +export const postgresAndMinioTest = withWarmup( + test.extend({ + postgresContainer: clonedPostgresContainer, + prisma: prismaFromContainer, + minioContainer: [bootWorkerMinio, { scope: "worker" }], + resetMinio: [minioReset, { auto: true }], + minioConfig, + }), + async ({ minioContainer }) => { + void minioContainer; + await getWorkerPostgresContainer(); + } +); diff --git a/internal-packages/testcontainers/src/warmup.test.ts b/internal-packages/testcontainers/src/warmup.test.ts new file mode 100644 index 00000000000..b0382dcd024 --- /dev/null +++ b/internal-packages/testcontainers/src/warmup.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, vi } from "vitest"; +import { clickhouseTest, containerTest } from "./index"; + +vi.setConfig({ testTimeout: 10_000 }); + +describe.skip("a skipped suite that touches the fixture first", () => { + containerTest("never runs", async ({ prisma }) => { + expect(prisma).toBeDefined(); + }); +}); + +describe("container fixture warmup", () => { + containerTest("the first test is not billed for the container boot", async ({ prisma }) => { + const rows = await prisma.$queryRawUnsafe>("SELECT 1 as ok"); + + expect(rows[0]?.ok).toBe(1); + }); + + containerTest("later tests still get a working fixture", async ({ prisma }) => { + const rows = await prisma.$queryRawUnsafe>("SELECT 2 as ok"); + + expect(rows[0]?.ok).toBe(2); + }); +}); + +describe("worker-scoped fixtures are warmed too", () => { + clickhouseTest("clickhouse is up before the first test", async ({ clickhouseClient }) => { + const rs = await clickhouseClient.query({ query: "SELECT 1 AS ok", format: "JSONEachRow" }); + const rows = await rs.json<{ ok: number }>(); + + expect(rows[0]?.ok).toBe(1); + }); +}); From 49aff3cb393c79a80b29b7e983bfd4238117aadc Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 12:42:48 +0100 Subject: [PATCH 32/98] fix(clickhouse): use compatible logs text index syntax (#4704) ## Summary Allow the logs search schema migration to run on ClickHouse versions that require text index options to be literals. ## Root cause The text index declared `lowerUTF8(search_text)` as a preprocessor option. Some ClickHouse versions reject that column expression while parsing index settings. The projected `search_text` is already normalized to lowercase before insertion, so removing the redundant preprocessor preserves search behavior. Verified with the task events search integration tests. --- .../clickhouse/schema/040_create_task_events_search_v2.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal-packages/clickhouse/schema/040_create_task_events_search_v2.sql b/internal-packages/clickhouse/schema/040_create_task_events_search_v2.sql index 6f526a8ef78..5817d5c35d3 100644 --- a/internal-packages/clickhouse/schema/040_create_task_events_search_v2.sql +++ b/internal-packages/clickhouse/schema/040_create_task_events_search_v2.sql @@ -36,7 +36,7 @@ CREATE TABLE IF NOT EXISTS trigger_dev.task_events_search_v2 INDEX idx_run_id run_id TYPE bloom_filter(0.001) GRANULARITY 1, INDEX idx_search_text search_text - TYPE text(tokenizer = 'ngrams', preprocessor = lowerUTF8(search_text)) + TYPE text(tokenizer = 'ngrams') ) ENGINE = ReplacingMergeTree PARTITION BY toDate(inserted_at) From 4dabfca1d599c142cd12f488f173e8d0af9df174 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 19 Aug 2026 13:44:55 +0100 Subject: [PATCH 33/98] feat(webapp,cli,core): list production project runtime updates (#4659) --- .changeset/list-project-runtime-updates.md | 6 + .../OrganizationSettingsSideMenu.tsx | 20 ++ .../components/primitives/ClipboardField.tsx | 3 +- .../app/components/primitives/CopyButton.tsx | 2 +- .../app/components/primitives/Label.tsx | 7 +- .../components/primitives/SettingsLayout.tsx | 14 +- apps/webapp/app/routes/[_].$.ts | 24 +- .../RuntimeUpdatesPage.tsx | 212 ++++++++++++++++++ .../route.tsx | 82 +++++++ .../route.tsx | 54 ++++- .../app/routes/api.v1.projects.runtimes.ts | 17 ++ .../services/projectRuntimeUpdates.server.ts | 130 +++++++++++ apps/webapp/app/utils/deeplinkPages.test.ts | 11 + apps/webapp/app/utils/deeplinkPages.ts | 19 +- apps/webapp/app/utils/pathBuilder.ts | 4 + packages/cli-v3/src/apiClient.ts | 14 ++ packages/cli-v3/src/cli/index.ts | 2 + .../cli-v3/src/commands/projects/index.ts | 10 + packages/cli-v3/src/commands/projects/list.ts | 95 ++++++++ packages/core/src/v3/schemas/api-type.test.ts | 21 +- packages/core/src/v3/schemas/api.ts | 47 ++++ 21 files changed, 770 insertions(+), 24 deletions(-) create mode 100644 .changeset/list-project-runtime-updates.md create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.runtime-updates/RuntimeUpdatesPage.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.runtime-updates/route.tsx create mode 100644 apps/webapp/app/routes/api.v1.projects.runtimes.ts create mode 100644 apps/webapp/app/services/projectRuntimeUpdates.server.ts create mode 100644 packages/cli-v3/src/commands/projects/index.ts create mode 100644 packages/cli-v3/src/commands/projects/list.ts diff --git a/.changeset/list-project-runtime-updates.md b/.changeset/list-project-runtime-updates.md new file mode 100644 index 00000000000..fe8c7479769 --- /dev/null +++ b/.changeset/list-project-runtime-updates.md @@ -0,0 +1,6 @@ +--- +"trigger.dev": patch +"@trigger.dev/core": patch +--- + +List the current Production runtime for every accessible project with `trigger projects list`. Add `--needs-update` to identify projects currently running Node.js 21. diff --git a/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx index 465346ad150..56b988e3e7f 100644 --- a/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx +++ b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx @@ -2,6 +2,7 @@ import { ArrowLeftIcon } from "@heroicons/react/24/solid"; import { BellIcon } from "~/assets/icons/BellIcon"; import { ChainLinkIcon } from "~/assets/icons/ChainLinkIcon"; import { CreditCardIcon } from "~/assets/icons/CreditCardIcon"; +import { FolderOpenIcon } from "~/assets/icons/FolderOpenIcon"; import { PadlockIcon } from "~/assets/icons/PadlockIcon"; import { UsageIcon } from "~/assets/icons/UsageIcon"; import { RolesIcon } from "~/assets/icons/RolesIcon"; @@ -16,6 +17,7 @@ import { cn } from "~/utils/cn"; import { organizationPath, organizationRolesPath, + organizationRuntimeUpdatesPath, organizationSettingsPath, organizationSlackIntegrationPath, organizationSsoPath, @@ -49,11 +51,13 @@ export function OrganizationSettingsSideMenu({ buildInfo, isUsingPlugin, isSsoUsingPlugin, + hasProjectRuntimeUpdate, }: { organization: MatchedOrganization; buildInfo: BuildInfo; isUsingPlugin: boolean; isSsoUsingPlugin: boolean; + hasProjectRuntimeUpdate: boolean; }) { const { isManagedCloud } = useFeatures(); const featureFlags = useFeatureFlags(); @@ -127,6 +131,22 @@ export function OrganizationSettingsSideMenu({ ) : null} )} + + + Runtime update available. + + ) : undefined + } + /> { diff --git a/apps/webapp/app/components/primitives/CopyButton.tsx b/apps/webapp/app/components/primitives/CopyButton.tsx index 91d47c29668..c855d4ad137 100644 --- a/apps/webapp/app/components/primitives/CopyButton.tsx +++ b/apps/webapp/app/components/primitives/CopyButton.tsx @@ -50,7 +50,7 @@ export function CopyButton({ onClick={copy} className={cn( buttonSize, - "flex items-center justify-center rounded border border-border-bright bg-background-hover", + "flex shrink-0 items-center justify-center rounded border border-border-bright bg-background-hover", copied ? "text-green-500" : "text-text-dimmed hover:border-border-bright hover:bg-background-raised hover:text-text-bright", diff --git a/apps/webapp/app/components/primitives/Label.tsx b/apps/webapp/app/components/primitives/Label.tsx index 7213d52205c..da27dee7eab 100644 --- a/apps/webapp/app/components/primitives/Label.tsx +++ b/apps/webapp/app/components/primitives/Label.tsx @@ -2,7 +2,8 @@ import * as React from "react"; import { cn } from "~/utils/cn"; import { InfoIconTooltip } from "./Tooltip"; -const variants = { +// Non-form labels (e.g. settings row titles) share this typography, so it is exported. +export const labelVariants = { small: { text: "font-sans text-[0.8125rem] font-normal text-text-bright leading-tight flex items-center gap-1", }, @@ -17,7 +18,7 @@ const variants = { type LabelProps = React.AllHTMLAttributes & { className?: string; children: React.ReactNode; - variant?: keyof typeof variants; + variant?: keyof typeof labelVariants; required?: boolean; tooltip?: React.ReactNode; }; @@ -30,7 +31,7 @@ export function Label({ tooltip, ...props }: LabelProps) { - const variation = variants[variant]; + const variation = labelVariants[variant]; return (