diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 827344f1f9..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -*/**.js -*/**.d.ts -packages/*/dist -packages/*/lib \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml index 279bab91a7..6760c37bcd 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -9,4 +9,4 @@ - any: ["**/*.md"] "πŸ“Œ area: ci": - - any: [".github/**/*"] \ No newline at end of file + - any: [".github/**/*"] diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000000..6fd2f73b4a --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,38 @@ +name: "🎨 Format & Lint" + +on: + workflow_call: + +permissions: + contents: read + +jobs: + code-quality: + runs-on: ubuntu-latest + + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: βŽ” Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + version: 10.33.2 + + - name: βŽ” Setup node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 20.20.2 + cache: "pnpm" + + - name: πŸ“₯ Download deps + run: pnpm install --frozen-lockfile + + - name: πŸ’… Check formatting + run: pnpm exec oxfmt --check . + + - name: πŸ”Ž Lint + run: pnpm exec oxlint . diff --git a/.github/workflows/dependabot-critical-alerts.yml b/.github/workflows/dependabot-critical-alerts.yml index e69e9c0af7..9a61387324 100644 --- a/.github/workflows/dependabot-critical-alerts.yml +++ b/.github/workflows/dependabot-critical-alerts.yml @@ -2,7 +2,7 @@ name: Dependabot Critical Alerts on: schedule: - - cron: "0 8 * * *" # Daily 08:00 UTC + - cron: "0 8 * * *" # Daily 08:00 UTC workflow_dispatch: inputs: severity: diff --git a/.github/workflows/dependabot-weekly-summary.yml b/.github/workflows/dependabot-weekly-summary.yml index 43463b488e..d7b489d55d 100644 --- a/.github/workflows/dependabot-weekly-summary.yml +++ b/.github/workflows/dependabot-weekly-summary.yml @@ -2,7 +2,7 @@ name: Dependabot Weekly Summary on: schedule: - - cron: "0 8 * * 1" # Mon 08:00 UTC + - cron: "0 8 * * 1" # Mon 08:00 UTC workflow_dispatch: # Single-purpose monitoring workflow; serialise on workflow name only - we never @@ -12,9 +12,9 @@ concurrency: cancel-in-progress: false permissions: - contents: read # gh CLI baseline - pull-requests: read # gh pr list (open dependabot PRs) - actions: read # gh run list / view (parse latest dependabot run logs) + contents: read # gh CLI baseline + pull-requests: read # gh pr list (open dependabot PRs) + actions: read # gh run list / view (parse latest dependabot run logs) jobs: summary: diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 9580553980..1fb3a47a45 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -102,6 +102,11 @@ jobs: - 'pnpm-workspace.yaml' - 'turbo.json' + code-quality: + needs: changes + if: needs.changes.outputs.code == 'true' + uses: ./.github/workflows/code-quality.yml + typecheck: needs: changes if: needs.changes.outputs.code == 'true' || needs.changes.outputs.typecheck_self == 'true' @@ -155,6 +160,7 @@ jobs: name: All PR Checks needs: - changes + - code-quality - typecheck - webapp - e2e-webapp diff --git a/.github/workflows/release-helm.yml b/.github/workflows/release-helm.yml index 6dbfee7d0c..d21c62de8b 100644 --- a/.github/workflows/release-helm.yml +++ b/.github/workflows/release-helm.yml @@ -3,17 +3,17 @@ name: 🧭 Helm Chart Release on: push: tags: - - 'helm-v*' + - "helm-v*" workflow_call: inputs: chart_version: - description: 'Chart version to release' + description: "Chart version to release" required: true type: string workflow_dispatch: inputs: chart_version: - description: 'Chart version to release' + description: "Chart version to release" required: true type: string @@ -58,7 +58,7 @@ jobs: - name: Validate manifests uses: docker://ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c with: - entrypoint: '/kubeconform' + entrypoint: "/kubeconform" args: "-summary -output json ./helm-output" release: @@ -134,7 +134,7 @@ jobs: run: | VERSION="${STEPS_VERSION_OUTPUTS_VERSION}" CHART_PACKAGE="/tmp/${{ env.CHART_NAME }}-${VERSION}.tgz" - + # Push to GHCR OCI registry helm push "$CHART_PACKAGE" "oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/charts" env: @@ -153,7 +153,7 @@ jobs: oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/charts/${{ env.CHART_NAME }} \ --version "${{ steps.version.outputs.version }}" ``` - + ### Changes See commit history for detailed changes in this release. files: | diff --git a/.github/workflows/workflow-checks.yml b/.github/workflows/workflow-checks.yml index e99a4d3342..62406671fc 100644 --- a/.github/workflows/workflow-checks.yml +++ b/.github/workflows/workflow-checks.yml @@ -4,14 +4,14 @@ on: push: branches: [main] paths: - - '.github/workflows/**' - - '.github/actions/**' - - '.github/zizmor.yml' + - ".github/workflows/**" + - ".github/actions/**" + - ".github/zizmor.yml" pull_request: paths: - - '.github/workflows/**' - - '.github/actions/**' - - '.github/zizmor.yml' + - ".github/workflows/**" + - ".github/actions/**" + - ".github/zizmor.yml" permissions: {} diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 2fcbb54012..e7920e8cf1 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -2,4 +2,4 @@ rules: unpinned-uses: config: policies: - '*': hash-pin + "*": hash-pin diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000000..71809f50ef --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,30 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "semi": true, + "singleQuote": false, + "jsxSingleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "bracketSameLine": false, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "sortPackageJson": false, + "ignorePatterns": [ + "node_modules", + ".env", + ".env.local", + "pnpm-lock.yaml", + "tailwind.css", + ".babelrc.json", + "**/.react-email/", + "**/storybook-static/", + "**/.changeset/", + "**/dist/", + "internal-packages/tsql/src/grammar/", + // Maybe turn these on in the future + "**/*.yaml", + "**/*.mdx", + "**/*.md" + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000000..99babf86a1 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,46 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "import", "react"], + "ignorePatterns": [ + "**/dist/**", + "**/build/**", + "**/*.d.ts", + "**/seed.js", + "**/seedCloud.ts", + "**/populate.js" + ], + // All rules below are disabled because they currently fire on the existing + // codebase (1748 warnings across these 20 rules as of this commit). Disabled + // to keep the lint baseline green; re-enable and fix incrementally. + "rules": { + // eslint + "no-unused-vars": "off", + "no-unused-expressions": "off", + "no-control-regex": "off", + "no-empty-pattern": "off", + "no-unused-private-class-members": "off", + "no-useless-catch": "off", + "no-unsafe-optional-chaining": "off", + "no-unreachable": "off", + "require-yield": "off", + "no-async-promise-executor": "off", + "no-unsafe-finally": "off", + "no-useless-escape": "off", + // typescript + "typescript/consistent-type-imports": "off", + "typescript/no-this-alias": "off", + "typescript/no-non-null-asserted-optional-chain": "off", + "typescript/no-unnecessary-parameter-property-assignment": "off", + // import + "import/no-duplicates": "off", + "import/namespace": "off", + // react + "react/jsx-key": "off", + "react/no-children-prop": "off", + // react-hooks + "react-hooks/exhaustive-deps": "off", + // oxlint treats any use*()/use() call as a React hook, producing false + // positives in async/test code (testcontainers, cli-v3 e2e). + "react-hooks/rules-of-hooks": "off" + } +} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index a34447dd45..0000000000 --- a/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules -.env -.env.local -pnpm-lock.yaml -tailwind.css -.babelrc.json -**/.react-email/ -**/storybook-static/ -**/.changeset/ -**/dist/ \ No newline at end of file diff --git a/apps/coordinator/package.json b/apps/coordinator/package.json index 3b4240bd37..446606a44c 100644 --- a/apps/coordinator/package.json +++ b/apps/coordinator/package.json @@ -27,4 +27,4 @@ "esbuild": "^0.19.11", "tsx": "^4.7.0" } -} \ No newline at end of file +} diff --git a/apps/docker-provider/package.json b/apps/docker-provider/package.json index f3e4015ef0..aa812c68d4 100644 --- a/apps/docker-provider/package.json +++ b/apps/docker-provider/package.json @@ -24,4 +24,4 @@ "esbuild": "^0.19.11", "tsx": "^4.7.0" } -} \ No newline at end of file +} diff --git a/apps/kubernetes-provider/package.json b/apps/kubernetes-provider/package.json index 6cb26e2c70..b4a4307abb 100644 --- a/apps/kubernetes-provider/package.json +++ b/apps/kubernetes-provider/package.json @@ -25,4 +25,4 @@ "esbuild": "^0.19.11", "tsx": "^4.7.0" } -} \ No newline at end of file +} diff --git a/apps/supervisor/src/backpressure/k8sPodCountSignalSource.test.ts b/apps/supervisor/src/backpressure/k8sPodCountSignalSource.test.ts index 8857372a04..8c7104cfb1 100644 --- a/apps/supervisor/src/backpressure/k8sPodCountSignalSource.test.ts +++ b/apps/supervisor/src/backpressure/k8sPodCountSignalSource.test.ts @@ -73,9 +73,9 @@ describe("K8sPodCountSignalSource", () => { engageThreshold: 10000, releaseThreshold: 5000, }); - expect((await source.read()).engaged).toBe(true); // engage + expect((await source.read()).engaged).toBe(true); // engage count = 7000; - expect((await source.read()).engaged).toBe(true); // band -> still engaged + expect((await source.read()).engaged).toBe(true); // band -> still engaged count = 4999; expect((await source.read()).engaged).toBe(false); // below release -> off count = 7000; diff --git a/apps/supervisor/src/backpressure/redisBackpressureSignalSource.test.ts b/apps/supervisor/src/backpressure/redisBackpressureSignalSource.test.ts index 77a7457b13..dc8040a4c5 100644 --- a/apps/supervisor/src/backpressure/redisBackpressureSignalSource.test.ts +++ b/apps/supervisor/src/backpressure/redisBackpressureSignalSource.test.ts @@ -49,14 +49,17 @@ describe("RedisBackpressureSignalSource", () => { } }); - redisTest("returns null for valid JSON of the wrong shape (fail-open)", async ({ redisOptions }) => { - const redis = new Redis(redisOptions); - try { - await redis.set(KEY, JSON.stringify({ foo: "bar" })); - const source = new RedisBackpressureSignalSource(redis, KEY); - expect(await source.read()).toBeNull(); - } finally { - await redis.quit(); + redisTest( + "returns null for valid JSON of the wrong shape (fail-open)", + async ({ redisOptions }) => { + const redis = new Redis(redisOptions); + try { + await redis.set(KEY, JSON.stringify({ foo: "bar" })); + const source = new RedisBackpressureSignalSource(redis, KEY); + expect(await source.read()).toBeNull(); + } finally { + await redis.quit(); + } } - }); + ); }); diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index 10e8f9b62b..2cb1bfb9a7 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -75,9 +75,21 @@ export const Env = z TRIGGER_DEQUEUE_BACKPRESSURE_REDIS_TLS_DISABLED: BoolEnv.default(false), TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_ENABLED: BoolEnv.default(false), TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_DRY_RUN: BoolEnv.default(true), - TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_ENGAGE: z.coerce.number().int().positive().default(10_000), - TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_RELEASE: z.coerce.number().int().positive().default(5_000), - TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_REFRESH_MS: z.coerce.number().int().positive().default(5_000), + TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_ENGAGE: z.coerce + .number() + .int() + .positive() + .default(10_000), + TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_RELEASE: z.coerce + .number() + .int() + .positive() + .default(5_000), + TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_REFRESH_MS: z.coerce + .number() + .int() + .positive() + .default(5_000), // Hard timeout on the apiserver /metrics scrape. A hung request would otherwise // never settle and freeze the monitor's refresh loop (fail-open silently). TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_SCRAPE_TIMEOUT_MS: z.coerce @@ -350,7 +362,10 @@ export const Env = z path: ["TRIGGER_WORKLOAD_API_DOMAIN"], }); } - if (data.TRIGGER_DEQUEUE_BACKPRESSURE_ENABLED && !data.TRIGGER_DEQUEUE_BACKPRESSURE_REDIS_HOST) { + if ( + data.TRIGGER_DEQUEUE_BACKPRESSURE_ENABLED && + !data.TRIGGER_DEQUEUE_BACKPRESSURE_REDIS_HOST + ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: diff --git a/apps/supervisor/src/index.ts b/apps/supervisor/src/index.ts index d2bc82c13b..833448ad87 100644 --- a/apps/supervisor/src/index.ts +++ b/apps/supervisor/src/index.ts @@ -274,7 +274,10 @@ class ManagedSupervisor { rampMs: env.TRIGGER_DEQUEUE_BACKPRESSURE_RAMP_MS, dryRun: env.TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_DRY_RUN, logger: this.logger, - metrics: new BackpressureMetrics({ register, prefix: "supervisor_backpressure_pod_count" }), + metrics: new BackpressureMetrics({ + register, + prefix: "supervisor_backpressure_pod_count", + }), }) ); this.logger.log("πŸ›‘ Dequeue backpressure enabled (pod-count source)", { diff --git a/apps/supervisor/src/services/computeSnapshotService.test.ts b/apps/supervisor/src/services/computeSnapshotService.test.ts index b039b63bd4..44a13a4bdd 100644 --- a/apps/supervisor/src/services/computeSnapshotService.test.ts +++ b/apps/supervisor/src/services/computeSnapshotService.test.ts @@ -10,7 +10,9 @@ const DELAY_MS = 200; const SETTLE_MS = 600; function createService() { - const snapshot = vi.fn(async (_opts: { runnerId: string; metadata: Record }) => true); + const snapshot = vi.fn( + async (_opts: { runnerId: string; metadata: Record }) => true + ); const computeManager = { snapshotDelayMs: DELAY_MS, @@ -97,9 +99,7 @@ describe("ComputeSnapshotService", () => { expect(service.cancel("run_1", "runner-b")).toBe(false); await vi.waitFor(() => expect(snapshot).toHaveBeenCalledTimes(1), { timeout: 2_000 }); - expect(snapshot).toHaveBeenCalledWith( - expect.objectContaining({ runnerId: "runner-a" }) - ); + expect(snapshot).toHaveBeenCalledWith(expect.objectContaining({ runnerId: "runner-a" })); } finally { service.stop(); } diff --git a/apps/supervisor/src/services/otlpTraceService.test.ts b/apps/supervisor/src/services/otlpTraceService.test.ts index baf3bd9030..95053021ca 100644 --- a/apps/supervisor/src/services/otlpTraceService.test.ts +++ b/apps/supervisor/src/services/otlpTraceService.test.ts @@ -31,9 +31,7 @@ describe("buildPayload", () => { expect(triggerAttr).toEqual({ key: "$trigger", value: { boolValue: true } }); // Resource attributes - const envAttr = resourceSpan.resource.attributes.find( - (a) => a.key === "ctx.environment.id" - ); + const envAttr = resourceSpan.resource.attributes.find((a) => a.key === "ctx.environment.id"); expect(envAttr).toEqual({ key: "ctx.environment.id", value: { stringValue: "env_123" }, diff --git a/apps/supervisor/src/services/warmStartVerificationService.test.ts b/apps/supervisor/src/services/warmStartVerificationService.test.ts index 994c678b4b..4a5f58875c 100644 --- a/apps/supervisor/src/services/warmStartVerificationService.test.ts +++ b/apps/supervisor/src/services/warmStartVerificationService.test.ts @@ -19,10 +19,7 @@ function makeMessage(runFriendlyId = "run_1"): DequeuedMessage { } as unknown as DequeuedMessage; } -function createService(opts: { - latestSnapshotId?: string; - probeError?: boolean; -}) { +function createService(opts: { latestSnapshotId?: string; probeError?: boolean }) { const getLatestSnapshot = vi.fn(async (_runId: string) => opts.probeError ? { success: false as const, error: "connection refused" } diff --git a/apps/supervisor/src/wideEvents/index.ts b/apps/supervisor/src/wideEvents/index.ts index 4eda429a50..6e61d85896 100644 --- a/apps/supervisor/src/wideEvents/index.ts +++ b/apps/supervisor/src/wideEvents/index.ts @@ -11,12 +11,7 @@ export { type Env, isValidRequestId, newState, type NewStateOptions } from "./ne 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 { type PhaseOpt, recordPhase, recordPhaseSince, timePhase } from "./record.js"; export { emitOneShot, runWideEvent, diff --git a/apps/supervisor/src/wideEvents/middleware.test.ts b/apps/supervisor/src/wideEvents/middleware.test.ts index afb59f43d6..a431df4ada 100644 --- a/apps/supervisor/src/wideEvents/middleware.test.ts +++ b/apps/supervisor/src/wideEvents/middleware.test.ts @@ -124,7 +124,8 @@ describe("runWideEvent", () => { { service: "supervisor", env: {}, - enabled: true, op: "test", + enabled: true, + op: "test", setup: (state) => { state.meta.run_id = "run_abc"; state.extras.iteration = "dequeue"; @@ -158,16 +159,13 @@ describe("runWideEvent", () => { const lines = await captureStdout(async () => { await Promise.all( ["a", "b", "c"].map((tag) => - runWideEvent( - { service: "supervisor", env: {}, enabled: true, op: "test" }, - async () => { - const s = fromContext(); - if (!s) throw new Error("no state"); - s.meta.tag = tag; - await new Promise((r) => setTimeout(r, 5)); - expect(s.meta.tag).toBe(tag); - } - ) + runWideEvent({ service: "supervisor", env: {}, enabled: true, op: "test" }, async () => { + const s = fromContext(); + if (!s) throw new Error("no state"); + s.meta.tag = tag; + await new Promise((r) => setTimeout(r, 5)); + expect(s.meta.tag).toBe(tag); + }) ) ); }); @@ -182,7 +180,8 @@ describe("emitOneShot", () => { emitOneShot({ service: "supervisor", env: {}, - enabled: true, op: "test", + enabled: true, + op: "test", populate: (s) => { s.meta.run_id = "run_abc"; s.extras.event = "run:start"; diff --git a/apps/supervisor/src/workloadManager/compute.test.ts b/apps/supervisor/src/workloadManager/compute.test.ts index ea5ddabf28..95a9ca2f3b 100644 --- a/apps/supervisor/src/workloadManager/compute.test.ts +++ b/apps/supervisor/src/workloadManager/compute.test.ts @@ -15,9 +15,7 @@ describe("runnerNameForAttempt", () => { describe("isRetryableCreateError", () => { it("retries statuses where the create definitely did not commit", () => { - expect(isRetryableCreateError(new ComputeClientError(500, "tap busy", "http://gw"))).toBe( - true - ); + expect(isRetryableCreateError(new ComputeClientError(500, "tap busy", "http://gw"))).toBe(true); expect(isRetryableCreateError(new ComputeClientError(503, "no placement", "http://gw"))).toBe( true ); diff --git a/apps/supervisor/src/workloadManager/compute.ts b/apps/supervisor/src/workloadManager/compute.ts index 7a4270513c..857e129a83 100644 --- a/apps/supervisor/src/workloadManager/compute.ts +++ b/apps/supervisor/src/workloadManager/compute.ts @@ -250,9 +250,7 @@ export class ComputeWorkloadManager implements WorkloadManager { // name registered, so subsequent attempts use a suffixed name. let suffixAttempts = false; for (; attempt <= this.createMaxAttempts; attempt++) { - const attemptRunnerId = suffixAttempts - ? runnerNameForAttempt(runnerId, attempt) - : runnerId; + const attemptRunnerId = suffixAttempts ? runnerNameForAttempt(runnerId, attempt) : runnerId; [error, data] = await tryCatch( this.compute.instances.create( attemptRunnerId === runnerId diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index 9e79fff76c..762860104b 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -431,7 +431,8 @@ export class KubernetesWorkloadManager implements WorkloadManager { // Only large machine affinity produces hard requirements (non-large runs must stay off the large pool). // Schedule affinity is soft both ways. const required = [ - ...(largeNodeAffinity?.requiredDuringSchedulingIgnoredDuringExecution?.nodeSelectorTerms ?? []), + ...(largeNodeAffinity?.requiredDuringSchedulingIgnoredDuringExecution?.nodeSelectorTerms ?? + []), ]; const hasNodeAffinity = preferred.length > 0 || required.length > 0; @@ -443,7 +444,9 @@ export class KubernetesWorkloadManager implements WorkloadManager { return { ...(hasNodeAffinity && { nodeAffinity: { - ...(preferred.length > 0 && { preferredDuringSchedulingIgnoredDuringExecution: preferred }), + ...(preferred.length > 0 && { + preferredDuringSchedulingIgnoredDuringExecution: preferred, + }), ...(required.length > 0 && { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: required }, }), @@ -497,7 +500,10 @@ export class KubernetesWorkloadManager implements WorkloadManager { } #getScheduleNodeAffinityRules(isScheduledRun: boolean): k8s.V1NodeAffinity | undefined { - if (!env.KUBERNETES_SCHEDULED_RUN_AFFINITY_ENABLED || !env.KUBERNETES_SCHEDULED_RUN_AFFINITY_POOL_LABEL_VALUE) { + if ( + !env.KUBERNETES_SCHEDULED_RUN_AFFINITY_ENABLED || + !env.KUBERNETES_SCHEDULED_RUN_AFFINITY_POOL_LABEL_VALUE + ) { return undefined; } diff --git a/apps/supervisor/src/workloadManager/types.ts b/apps/supervisor/src/workloadManager/types.ts index 86199afe46..ee759cb8a7 100644 --- a/apps/supervisor/src/workloadManager/types.ts +++ b/apps/supervisor/src/workloadManager/types.ts @@ -1,4 +1,9 @@ -import type { EnvironmentType, MachinePreset, PlacementTag, RunAnnotations } from "@trigger.dev/core/v3"; +import type { + EnvironmentType, + MachinePreset, + PlacementTag, + RunAnnotations, +} from "@trigger.dev/core/v3"; export interface WorkloadManagerOptions { workloadApiProtocol: "http" | "https"; diff --git a/apps/supervisor/src/workloadServer/index.ts b/apps/supervisor/src/workloadServer/index.ts index 5dd1a79cc9..7397313080 100644 --- a/apps/supervisor/src/workloadServer/index.ts +++ b/apps/supervisor/src/workloadServer/index.ts @@ -317,9 +317,7 @@ export class WorkloadServer extends EventEmitter { return; } - reply.json( - completeResponse.data satisfies WorkloadRunAttemptCompleteResponseBody - ); + reply.json(completeResponse.data satisfies WorkloadRunAttemptCompleteResponseBody); return; } ), @@ -566,7 +564,9 @@ export class WorkloadServer extends EventEmitter { } reply.json( - dequeueResponse.data.map(legacifyCheckpointType) satisfies WorkloadDequeueFromVersionResponseBody + dequeueResponse.data.map( + legacifyCheckpointType + ) satisfies WorkloadDequeueFromVersionResponseBody ); } ), @@ -613,16 +613,22 @@ export class WorkloadServer extends EventEmitter { httpServer.route("/api/v1/compute/snapshot-complete", "POST", { bodySchema: SnapshotCallbackPayloadSchema, handler: async (ctx) => - this.wideRoute(ctx, "snapshot.callback", "/api/v1/compute/snapshot-complete", "POST", async () => { - const { reply, body } = ctx; - if (!this.snapshotService) { - reply.empty(404); - return; - } + this.wideRoute( + ctx, + "snapshot.callback", + "/api/v1/compute/snapshot-complete", + "POST", + async () => { + const { reply, body } = ctx; + if (!this.snapshotService) { + reply.empty(404); + return; + } - const result = await this.snapshotService.handleCallback(body); - reply.empty(result.status); - }), + const result = await this.snapshotService.handleCallback(body); + reply.empty(result.status); + } + ), }); return httpServer; diff --git a/apps/webapp/.eslintrc b/apps/webapp/.eslintrc deleted file mode 100644 index f292eef3cc..0000000000 --- a/apps/webapp/.eslintrc +++ /dev/null @@ -1,31 +0,0 @@ -{ - "plugins": ["react-hooks", "@typescript-eslint/eslint-plugin", "import"], - "parser": "@typescript-eslint/parser", - "overrides": [ - { - "files": ["*.ts", "*.tsx"], - "rules": { - // Autofixes imports from "@trigger.dev/core" to fine grained modules - // "@trigger.dev/no-trigger-core-import": "error", - // Normalize `import type {}` and `import { type }` - "@typescript-eslint/consistent-type-imports": [ - "warn", - { - // the "type" annotation can get tangled and cause syntax errors - // during some autofixes, so easier to just turn it off - "prefer": "type-imports", - "disallowTypeAnnotations": true, - "fixStyle": "inline-type-imports" - } - ], - // no-trigger-core-import splits imports into multiple lines - // this one merges them back into a single line - // if they still import from the same module - "import/no-duplicates": ["warn", { "prefer-inline": true }], - // lots of undeclared vars, enable this rule if you want to clean them up - "turbo/no-undeclared-env-vars": "off" - } - } - ], - "ignorePatterns": ["seed.js", "seedCloud.ts", "populate.js"] -} diff --git a/apps/webapp/.prettierignore b/apps/webapp/.prettierignore deleted file mode 100644 index 835d1a6cdd..0000000000 --- a/apps/webapp/.prettierignore +++ /dev/null @@ -1,11 +0,0 @@ -node_modules - -/build -/public/build -.env - -/cypress/screenshots -/cypress/videos -/postgres-data - -/app/styles/tailwind.css \ No newline at end of file diff --git a/apps/webapp/app/assets/icons/AIPenIcon.tsx b/apps/webapp/app/assets/icons/AIPenIcon.tsx index 65f6fdb2f3..146edd0e6d 100644 --- a/apps/webapp/app/assets/icons/AIPenIcon.tsx +++ b/apps/webapp/app/assets/icons/AIPenIcon.tsx @@ -19,13 +19,7 @@ export function AIPenIcon({ className }: { className?: string }) { stroke="currentColor" strokeWidth="2" /> - + ); } diff --git a/apps/webapp/app/assets/icons/AiProviderIcons.tsx b/apps/webapp/app/assets/icons/AiProviderIcons.tsx index 2be3fe38ed..418cdeff56 100644 --- a/apps/webapp/app/assets/icons/AiProviderIcons.tsx +++ b/apps/webapp/app/assets/icons/AiProviderIcons.tsx @@ -147,7 +147,12 @@ export function CerebrasIcon({ className }: IconProps) { export function MistralIcon({ className }: IconProps) { return ( - + @@ -174,4 +179,3 @@ export function AzureIcon({ className }: IconProps) { ); } - diff --git a/apps/webapp/app/assets/icons/AttemptIcon.tsx b/apps/webapp/app/assets/icons/AttemptIcon.tsx index de2a886b9d..4b6c7f0369 100644 --- a/apps/webapp/app/assets/icons/AttemptIcon.tsx +++ b/apps/webapp/app/assets/icons/AttemptIcon.tsx @@ -8,22 +8,8 @@ export function AttemptIcon({ className }: { className?: string }) { fill="none" xmlns="http://www.w3.org/2000/svg" > - - + + - + ); } diff --git a/apps/webapp/app/assets/icons/FunctionIcon.tsx b/apps/webapp/app/assets/icons/FunctionIcon.tsx index 2defe9c2f8..11b630ac2f 100644 --- a/apps/webapp/app/assets/icons/FunctionIcon.tsx +++ b/apps/webapp/app/assets/icons/FunctionIcon.tsx @@ -8,15 +8,7 @@ export function FunctionIcon({ className }: { className?: string }) { fill="none" xmlns="http://www.w3.org/2000/svg" > - + - + - + setHovered(true)} onMouseLeave={() => setHovered(false)} > - + - + + diff --git a/apps/webapp/app/assets/icons/StreamsIcon.tsx b/apps/webapp/app/assets/icons/StreamsIcon.tsx index 73cc480f4d..8deb7a19c9 100644 --- a/apps/webapp/app/assets/icons/StreamsIcon.tsx +++ b/apps/webapp/app/assets/icons/StreamsIcon.tsx @@ -1,10 +1,24 @@ export function StreamsIcon({ className }: { className?: string }) { return ( - - - - - + + + + + ); } - diff --git a/apps/webapp/app/assets/icons/TextSquareIcon.tsx b/apps/webapp/app/assets/icons/TextSquareIcon.tsx index d25faf6d0b..cffb0aa9d2 100644 --- a/apps/webapp/app/assets/icons/TextSquareIcon.tsx +++ b/apps/webapp/app/assets/icons/TextSquareIcon.tsx @@ -9,10 +9,42 @@ export function TextSquareIcon({ className }: { className?: string }) { xmlns="http://www.w3.org/2000/svg" > - - - - + + + + ); } diff --git a/apps/webapp/app/clientBeforeFirstRender.ts b/apps/webapp/app/clientBeforeFirstRender.ts index 3275c54423..5b5b640422 100644 --- a/apps/webapp/app/clientBeforeFirstRender.ts +++ b/apps/webapp/app/clientBeforeFirstRender.ts @@ -22,10 +22,7 @@ function cleanupLegacyResizablePanelStorage() { const toRemove: string[] = []; for (let i = 0; i < window.localStorage.length; i++) { const key = window.localStorage.key(i); - if ( - key && - (key.startsWith("panel-group-react-aria") || key === "panel-run-parent-v2") - ) { + if (key && (key.startsWith("panel-group-react-aria") || key === "panel-run-parent-v2")) { toRemove.push(key); } } diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx index 814d4649c8..bf76654cce 100644 --- a/apps/webapp/app/components/AskAI.tsx +++ b/apps/webapp/app/components/AskAI.tsx @@ -134,11 +134,7 @@ function AskAIProvider({ websiteId, isCollapsed = false }: AskAIProviderProps) { - + Ask AI diff --git a/apps/webapp/app/components/BlankStatePanels.tsx b/apps/webapp/app/components/BlankStatePanels.tsx index fc926266fd..7b5bdc6310 100644 --- a/apps/webapp/app/components/BlankStatePanels.tsx +++ b/apps/webapp/app/components/BlankStatePanels.tsx @@ -208,17 +208,17 @@ export function SessionsNone() { } > - A session is a stateful execution of an agent, with two-way streaming and durable - compute. A single session can have multiple runs associated with it, so one conversation - can span many task triggers. The input stream carries incoming user messages, and the - output stream carries everything the agent produces, including AI generation parts (text, - reasoning, tool calls, etc.) and any custom data parts your task emits. + A session is a stateful execution of an agent, with two-way streaming and durable compute. A + single session can have multiple runs associated with it, so one conversation can span many + task triggers. The input stream carries incoming user messages, and the output stream + carries everything the agent produces, including AI generation parts (text, reasoning, tool + calls, etc.) and any custom data parts your task emits. The easiest way to create one is to trigger a chat.agent task, which is built on sessions and handles the chat turn loop for you. You can also call{" "} - sessions.start() directly for non-chat patterns like agent - inboxes, approval flows, or server-to-server streaming. + sessions.start() directly for non-chat patterns like agent inboxes, + approval flows, or server-to-server streaming. ); diff --git a/apps/webapp/app/components/DevPresence.tsx b/apps/webapp/app/components/DevPresence.tsx index 7c67d56890..27d9695475 100644 --- a/apps/webapp/app/components/DevPresence.tsx +++ b/apps/webapp/app/components/DevPresence.tsx @@ -154,8 +154,8 @@ export function DevPresencePanel({ isConnected }: { isConnected: boolean | undef {isConnected === undefined ? "Checking connection..." : isConnected - ? "Your dev server is connected" - : "Your dev server is not connected"} + ? "Your dev server is connected" + : "Your dev server is not connected"}
@@ -169,8 +169,8 @@ export function DevPresencePanel({ isConnected }: { isConnected: boolean | undef {isConnected === undefined ? "Checking connection..." : isConnected - ? "Your local dev server is connected to Trigger.dev" - : "Your local dev server is not connected to Trigger.dev"} + ? "Your local dev server is connected to Trigger.dev" + : "Your local dev server is not connected to Trigger.dev"}
{isConnected ? null : ( diff --git a/apps/webapp/app/components/LoginPageLayout.tsx b/apps/webapp/app/components/LoginPageLayout.tsx index 3e42cd6894..323faf4ea2 100644 --- a/apps/webapp/app/components/LoginPageLayout.tsx +++ b/apps/webapp/app/components/LoginPageLayout.tsx @@ -63,8 +63,8 @@ export function LoginPageLayout({ children }: { children: React.ReactNode }) {
{children}
- Having login issues? Email us{" "} - or ask us in Discord + Having login issues? Email us or{" "} + ask us in Discord diff --git a/apps/webapp/app/components/SetupCommands.tsx b/apps/webapp/app/components/SetupCommands.tsx index accb2f65a8..d219b0be72 100644 --- a/apps/webapp/app/components/SetupCommands.tsx +++ b/apps/webapp/app/components/SetupCommands.tsx @@ -209,7 +209,10 @@ export function TriggerLoginStepV3({ title }: TabsProps) { ); } -export function TriggerDeployStep({ title, environment }: TabsProps & { environment: { type: string } }) { +export function TriggerDeployStep({ + title, + environment, +}: TabsProps & { environment: { type: string } }) { const triggerCliTag = useTriggerCliTag(); const { activePackageManager, setActivePackageManager } = usePackageManager(); diff --git a/apps/webapp/app/components/Shortcuts.tsx b/apps/webapp/app/components/Shortcuts.tsx index 4702f37239..63b5fddf71 100644 --- a/apps/webapp/app/components/Shortcuts.tsx +++ b/apps/webapp/app/components/Shortcuts.tsx @@ -4,13 +4,7 @@ import { useShortcutKeys } from "~/hooks/useShortcutKeys"; import { Button } from "./primitives/Buttons"; import { Header3 } from "./primitives/Headers"; import { Paragraph } from "./primitives/Paragraph"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetTrigger -} from "./primitives/SheetV3"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "./primitives/SheetV3"; import { ShortcutKey } from "./primitives/ShortcutKey"; export function Shortcuts() { @@ -83,7 +77,7 @@ function ShortcutContent() { - + diff --git a/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.server.ts b/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.server.ts index 4855c4c246..7de475bda8 100644 --- a/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.server.ts +++ b/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.server.ts @@ -40,9 +40,7 @@ export const apiRateLimitDomain: RateLimitDomain = { }, }; -export function resolveEffectiveApiRateLimit( - override: unknown -): EffectiveRateLimit { +export function resolveEffectiveApiRateLimit(override: unknown): EffectiveRateLimit { return resolveEffectiveRateLimit(override, apiRateLimitDomain); } diff --git a/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.tsx b/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.tsx index b27956f436..6b13bac8b4 100644 --- a/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.tsx +++ b/apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.tsx @@ -1,17 +1,8 @@ -import { - RateLimitSection, - type RateLimitWrapperProps, -} from "./RateLimitSection"; +import { RateLimitSection, type RateLimitWrapperProps } from "./RateLimitSection"; export const API_RATE_LIMIT_INTENT = "set-rate-limit"; export const API_RATE_LIMIT_SAVED_VALUE = "rate-limit"; export function ApiRateLimitSection(props: RateLimitWrapperProps) { - return ( - - ); + return ; } diff --git a/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.server.ts b/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.server.ts index 83a368094a..3891e4fc40 100644 --- a/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.server.ts +++ b/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.server.ts @@ -40,9 +40,7 @@ export const batchRateLimitDomain: RateLimitDomain = { }, }; -export function resolveEffectiveBatchRateLimit( - override: unknown -): EffectiveRateLimit { +export function resolveEffectiveBatchRateLimit(override: unknown): EffectiveRateLimit { return resolveEffectiveRateLimit(override, batchRateLimitDomain); } diff --git a/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.tsx b/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.tsx index 0e52124d29..64ee6a05d4 100644 --- a/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.tsx +++ b/apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.tsx @@ -1,17 +1,8 @@ -import { - RateLimitSection, - type RateLimitWrapperProps, -} from "./RateLimitSection"; +import { RateLimitSection, type RateLimitWrapperProps } from "./RateLimitSection"; export const BATCH_RATE_LIMIT_INTENT = "set-batch-rate-limit"; export const BATCH_RATE_LIMIT_SAVED_VALUE = "batch-rate-limit"; export function BatchRateLimitSection(props: RateLimitWrapperProps) { - return ( - - ); + return ; } diff --git a/apps/webapp/app/components/admin/backOffice/MaxProjectsSection.tsx b/apps/webapp/app/components/admin/backOffice/MaxProjectsSection.tsx index bf8ecf8316..8aa4b5c93c 100644 --- a/apps/webapp/app/components/admin/backOffice/MaxProjectsSection.tsx +++ b/apps/webapp/app/components/admin/backOffice/MaxProjectsSection.tsx @@ -68,9 +68,7 @@ export function MaxProjectsSection({ Limit - - {maximumProjectCount.toLocaleString()} - + {maximumProjectCount.toLocaleString()} ) : ( @@ -89,11 +87,7 @@ export function MaxProjectsSection({ {fieldError("maximumProjectCount")}
-
{isLoading ? ( diff --git a/apps/webapp/app/components/code/QueryResultsChart.tsx b/apps/webapp/app/components/code/QueryResultsChart.tsx index 2c3f1c9f2b..97013c192a 100644 --- a/apps/webapp/app/components/code/QueryResultsChart.tsx +++ b/apps/webapp/app/components/code/QueryResultsChart.tsx @@ -872,10 +872,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({ ); // Create value formatter for tooltips and legend based on column format - const tooltipValueFormatter = useMemo( - () => createValueFormatter(yAxisFormat), - [yAxisFormat] - ); + const tooltipValueFormatter = useMemo(() => createValueFormatter(yAxisFormat), [yAxisFormat]); // Check if the group-by column has a runStatus customRenderType const groupByIsRunStatus = useMemo(() => { @@ -1181,9 +1178,7 @@ function createYAxisFormatter( if (format === "bytes" || format === "decimalBytes") { const divisor = format === "bytes" ? 1024 : 1000; const units = - format === "bytes" - ? ["B", "KiB", "MiB", "GiB", "TiB"] - : ["B", "KB", "MB", "GB", "TB"]; + format === "bytes" ? ["B", "KiB", "MiB", "GiB", "TiB"] : ["B", "KB", "MB", "GB", "TB"]; return (value: number): string => { if (value === 0) return "0 B"; // Use consistent unit for all ticks based on max value @@ -1205,8 +1200,7 @@ function createYAxisFormatter( } if (format === "durationSeconds") { - return (value: number): string => - formatDurationMilliseconds(value * 1000, { style: "short" }); + return (value: number): string => formatDurationMilliseconds(value * 1000, { style: "short" }); } if (format === "durationNs") { diff --git a/apps/webapp/app/components/code/TSQLResultsTable.tsx b/apps/webapp/app/components/code/TSQLResultsTable.tsx index 73ca07180b..8e10f8e6fa 100644 --- a/apps/webapp/app/components/code/TSQLResultsTable.tsx +++ b/apps/webapp/app/components/code/TSQLResultsTable.tsx @@ -186,10 +186,10 @@ const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { cellValue === null ? "NULL" : cellValue === undefined - ? "" - : typeof cellValue === "object" - ? JSON.stringify(cellValue) - : String(cellValue); + ? "" + : typeof cellValue === "object" + ? JSON.stringify(cellValue) + : String(cellValue); // Build searchable strings - formatted value (if we have column metadata) const formattedValue = meta?.outputColumn @@ -569,12 +569,8 @@ function CellValue({ if (typeof value === "string") { const spanId = row?.["span_id"]; const runPath = v3RunPathFromFriendlyId(value); - const href = typeof spanId === "string" && spanId - ? `${runPath}?span=${spanId}` - : runPath; - const tooltip = typeof spanId === "string" && spanId - ? "Jump to span" - : "Jump to run"; + const href = typeof spanId === "string" && spanId ? `${runPath}?span=${spanId}` : runPath; + const tooltip = typeof spanId === "string" && spanId ? "Jump to span" : "Jump to run"; return ( hiddenColumns?.length ? columns.filter((col) => !hiddenColumns.includes(col.name)) : columns, + () => + hiddenColumns?.length ? columns.filter((col) => !hiddenColumns.includes(col.name)) : columns, [columns, hiddenColumns] ); const columnDefs = useMemo[]>( diff --git a/apps/webapp/app/components/code/codeMirrorTheme.ts b/apps/webapp/app/components/code/codeMirrorTheme.ts index 2faed6eaa5..c9dd2f12e8 100644 --- a/apps/webapp/app/components/code/codeMirrorTheme.ts +++ b/apps/webapp/app/components/code/codeMirrorTheme.ts @@ -67,10 +67,9 @@ export function darkTheme(): Extension { }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor }, - "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": - { - backgroundColor: selection, - }, + "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { + backgroundColor: selection, + }, ".cm-panels": { backgroundColor: darkBackground, color: ivory }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, @@ -167,20 +166,14 @@ export function darkTheme(): Extension { backgroundColor: scrollbarBg, }, }, - { dark: true }, + { dark: true } ); /// The highlighting style for code in the JSON Hero theme. const jsonHeroHighlightStyle = HighlightStyle.define([ { tag: tags.keyword, color: violet }, { - tag: [ - tags.name, - tags.deleted, - tags.character, - tags.propertyName, - tags.macroName, - ], + tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: lilac, }, { tag: [tags.function(tags.variableName), tags.labelName], color: malibu }, diff --git a/apps/webapp/app/components/code/tsql/index.ts b/apps/webapp/app/components/code/tsql/index.ts index bda244e30f..71c543161d 100644 --- a/apps/webapp/app/components/code/tsql/index.ts +++ b/apps/webapp/app/components/code/tsql/index.ts @@ -2,5 +2,9 @@ // Provides syntax highlighting, autocompletion, and linting for TSQL queries export { createTSQLCompletion } from "./tsqlCompletion"; -export { createTSQLLinter, isValidTSQLQuery, getTSQLError, type TSQLLinterConfig } from "./tsqlLinter"; - +export { + createTSQLLinter, + isValidTSQLQuery, + getTSQLError, + type TSQLLinterConfig, +} from "./tsqlLinter"; diff --git a/apps/webapp/app/components/code/tsql/tsqlCompletion.ts b/apps/webapp/app/components/code/tsql/tsqlCompletion.ts index b53c551a4d..03d75f179d 100644 --- a/apps/webapp/app/components/code/tsql/tsqlCompletion.ts +++ b/apps/webapp/app/components/code/tsql/tsqlCompletion.ts @@ -92,8 +92,8 @@ function createFunctionCompletions(): Completion[] { meta.maxArgs === 0 ? "()" : meta.minArgs === meta.maxArgs - ? `(${meta.minArgs} args)` - : `(${meta.minArgs}${meta.maxArgs ? `-${meta.maxArgs}` : "+"} args)`; + ? `(${meta.minArgs} args)` + : `(${meta.minArgs}${meta.maxArgs ? `-${meta.maxArgs}` : "+"} args)`; functions.push({ label: name, @@ -111,8 +111,8 @@ function createFunctionCompletions(): Completion[] { meta.maxArgs === 0 ? "()" : meta.minArgs === meta.maxArgs - ? `(${meta.minArgs} args)` - : `(${meta.minArgs}${meta.maxArgs ? `-${meta.maxArgs}` : "+"} args)`; + ? `(${meta.minArgs} args)` + : `(${meta.minArgs}${meta.maxArgs ? `-${meta.maxArgs}` : "+"} args)`; functions.push({ label: name, diff --git a/apps/webapp/app/components/code/tsql/tsqlLinter.test.ts b/apps/webapp/app/components/code/tsql/tsqlLinter.test.ts index 8acc81d66f..8ecc21a165 100644 --- a/apps/webapp/app/components/code/tsql/tsqlLinter.test.ts +++ b/apps/webapp/app/components/code/tsql/tsqlLinter.test.ts @@ -28,9 +28,7 @@ describe("tsqlLinter", () => { true ); expect( - isValidTSQLQuery( - "SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id" - ) + isValidTSQLQuery("SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id") ).toBe(true); }); @@ -76,4 +74,3 @@ describe("tsqlLinter", () => { }); }); }); - diff --git a/apps/webapp/app/components/code/tsql/tsqlLinter.ts b/apps/webapp/app/components/code/tsql/tsqlLinter.ts index 72794e3e9a..42f5288fb2 100644 --- a/apps/webapp/app/components/code/tsql/tsqlLinter.ts +++ b/apps/webapp/app/components/code/tsql/tsqlLinter.ts @@ -31,11 +31,7 @@ function parseErrorPosition(message: string): { line: number; column: number } | /** * Convert line/column to a document position */ -function positionToOffset( - doc: string, - line: number, - column: number -): number { +function positionToOffset(doc: string, line: number, column: number): number { const lines = doc.split("\n"); // line is 1-indexed @@ -210,4 +206,3 @@ export function getTSQLError(query: string): string | null { return "Unknown error"; } } - diff --git a/apps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsx b/apps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsx index a0a96ecb5e..2fef1f3deb 100644 --- a/apps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsx +++ b/apps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsx @@ -96,7 +96,14 @@ function DiagnosisActions({ actions }: { actions: NonNullable {actions.map((action, i) => { if (action.kind === "view_run" && /^run_[a-z0-9]+$/i.test(action.target)) { - return ; + return ( + + ); } if (action.kind === "docs") { const safeUrl = toSafeUrl(action.target); diff --git a/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx b/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx index 32ed778f87..8a823ee78c 100644 --- a/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx +++ b/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx @@ -135,12 +135,7 @@ export function ConfigureErrorAlerts({ /> - +
diff --git a/apps/webapp/app/components/integrations/VercelBuildSettings.tsx b/apps/webapp/app/components/integrations/VercelBuildSettings.tsx index d8e9f3fe3f..8449d342f5 100644 --- a/apps/webapp/app/components/integrations/VercelBuildSettings.tsx +++ b/apps/webapp/app/components/integrations/VercelBuildSettings.tsx @@ -113,9 +113,7 @@ export function BuildSettingsFields({
); if (disabled && disabledReason) { - return ( - - ); + return ; } return row; })} @@ -140,9 +138,7 @@ export function BuildSettingsFields({ disabled={!enabledSlugs.some((s) => pullEnvVarsBeforeBuild.includes(s))} onCheckedChange={(checked) => { onDiscoverEnvVarsChange( - checked - ? enabledSlugs.filter((s) => pullEnvVarsBeforeBuild.includes(s)) - : [] + checked ? enabledSlugs.filter((s) => pullEnvVarsBeforeBuild.includes(s)) : [] ); }} /> @@ -185,9 +181,7 @@ export function BuildSettingsFields({
); if (disabled && disabledReason) { - return ( - - ); + return ; } return row; })} @@ -208,10 +202,12 @@ export function BuildSettingsFields({ When enabled, production deployments wait for Vercel deployment to complete before - promoting the Trigger.dev deployment. This will disable the "Auto-assign Custom - Production Domains" option in your Vercel project settings to perform staged - deployments.{" "} - + promoting the Trigger.dev deployment. This will disable the "Auto-assign Custom Production + Domains" option in your Vercel project settings to perform staged deployments.{" "} + Learn more . @@ -225,9 +221,8 @@ export function BuildSettingsFields({ )} {!currentTriggerVersion && currentTriggerVersionFetchFailed && ( - Couldn't read{" "} - TRIGGER_VERSION from Vercel β€” - check the Vercel dashboard to confirm the production pin. + Couldn't read TRIGGER_VERSION from + Vercel β€” check the Vercel dashboard to confirm the production pin. )} @@ -244,9 +239,9 @@ export function BuildSettingsFields({ /> - When enabled, the integration automatically promotes the Vercel deployment after - the Trigger.dev build completes. Turn off to manually promote from your Vercel - dashboard β€” Trigger.dev will then promote automatically once you do. + When enabled, the integration automatically promotes the Vercel deployment after the + Trigger.dev build completes. Turn off to manually promote from your Vercel dashboard β€” + Trigger.dev will then promote automatically once you do. )} diff --git a/apps/webapp/app/components/integrations/VercelLogo.tsx b/apps/webapp/app/components/integrations/VercelLogo.tsx index 7ddf039abf..856b74ebad 100644 --- a/apps/webapp/app/components/integrations/VercelLogo.tsx +++ b/apps/webapp/app/components/integrations/VercelLogo.tsx @@ -1,11 +1,6 @@ export function VercelLogo({ className }: { className?: string }) { return ( - + ); diff --git a/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx b/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx index 21734c5c03..7ae12b20d4 100644 --- a/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx +++ b/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx @@ -4,11 +4,7 @@ import { ChevronDownIcon, ChevronUpIcon, } from "@heroicons/react/20/solid"; -import { - useFetcher, - useNavigation, - useSearchParams, -} from "@remix-run/react"; +import { useFetcher, useNavigation, useSearchParams } from "@remix-run/react"; import { useTypedFetcher } from "remix-typedjson"; import { Dialog, DialogContent, DialogHeader } from "~/components/primitives/Dialog"; import { Button, LinkButton } from "~/components/primitives/Buttons"; @@ -31,9 +27,7 @@ import { import { VercelLogo } from "~/components/integrations/VercelLogo"; import { BuildSettingsFields } from "~/components/integrations/VercelBuildSettings"; import { OctoKitty } from "~/components/GitHubLoginButton"; -import { - ConnectGitHubRepoModal, -} from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github"; +import { ConnectGitHubRepoModal } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github"; import { type SyncEnvVarsMapping, type EnvSlug, @@ -44,7 +38,12 @@ import { } from "~/v3/vercel/vercelProjectIntegrationSchema"; import { type VercelCustomEnvironment } from "~/models/vercelIntegration.server"; import { type VercelOnboardingData } from "~/presenters/v3/VercelSettingsPresenter.server"; -import { vercelAppInstallPath, v3ProjectSettingsIntegrationsPath, githubAppInstallPath, vercelResourcePath } from "~/utils/pathBuilder"; +import { + vercelAppInstallPath, + v3ProjectSettingsIntegrationsPath, + githubAppInstallPath, + vercelResourcePath, +} from "~/utils/pathBuilder"; import type { loader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel"; import { useEffect, useState, useCallback, useRef } from "react"; import { usePostHogTracking } from "~/hooks/usePostHog"; @@ -73,9 +72,7 @@ function formatVercelTargets(targets: string[]): string { staging: "Staging", }; - return targets - .map((t) => targetLabels[t.toLowerCase()] || t) - .join(", "); + return targets.map((t) => targetLabels[t.toLowerCase()] || t).join(", "); } type OnboardingState = @@ -153,7 +150,8 @@ export function VercelOnboardingModal({ } // For marketplace origin, skip env-mapping step and go directly to env-var-sync if (!fromMarketplaceContext) { - const customEnvs = (onboardingData?.customEnvironments?.length ?? 0) > 0 && hasStagingEnvironment; + const customEnvs = + (onboardingData?.customEnvironments?.length ?? 0) > 0 && hasStagingEnvironment; if (customEnvs) { return "env-mapping"; } @@ -218,14 +216,18 @@ export function VercelOnboardingModal({ environmentId: string; displayName: string; } | null>(null); - const availableEnvSlugsForOnboarding = getAvailableEnvSlugs(hasStagingEnvironment, hasPreviewEnvironment); - const availableEnvSlugsForOnboardingBuildSettings = getAvailableEnvSlugsForBuildSettings(hasStagingEnvironment, hasPreviewEnvironment); + const availableEnvSlugsForOnboarding = getAvailableEnvSlugs( + hasStagingEnvironment, + hasPreviewEnvironment + ); + const availableEnvSlugsForOnboardingBuildSettings = getAvailableEnvSlugsForBuildSettings( + hasStagingEnvironment, + hasPreviewEnvironment + ); const [pullEnvVarsBeforeBuild, setPullEnvVarsBeforeBuild] = useState( () => availableEnvSlugsForOnboardingBuildSettings ); - const [atomicBuilds, setAtomicBuilds] = useState( - () => ["prod"] - ); + const [atomicBuilds, setAtomicBuilds] = useState(() => ["prod"]); const [discoverEnvVars, setDiscoverEnvVars] = useState( () => availableEnvSlugsForOnboardingBuildSettings ); @@ -309,7 +311,13 @@ export function VercelOnboardingModal({ }, 100); } } - }, [isOpen, fromMarketplaceContext, nextUrl, isOnboardingComplete, isGitHubConnectedForOnboarding]); + }, [ + isOpen, + fromMarketplaceContext, + nextUrl, + isOnboardingComplete, + isGitHubConnectedForOnboarding, + ]); useEffect(() => { if (!isOpen) { @@ -336,7 +344,6 @@ export function VercelOnboardingModal({ } switch (state) { - case "loading-projects": loadingStateRef.current = state; if (onDataReload) { @@ -371,19 +378,33 @@ export function VercelOnboardingModal({ }, [isOpen, state, onboardingData?.authInvalid, vercelStagingEnvironment, onDataReload, onClose]); useEffect(() => { - if (!onboardingData?.authInvalid && state === "loading-projects" && onboardingData?.availableProjects !== undefined) { + if ( + !onboardingData?.authInvalid && + state === "loading-projects" && + onboardingData?.availableProjects !== undefined + ) { setState("project-selection"); } }, [state, onboardingData?.availableProjects, onboardingData?.authInvalid]); useEffect(() => { - if (!onboardingData?.authInvalid && state === "loading-env-vars" && onboardingData?.environmentVariables) { + if ( + !onboardingData?.authInvalid && + state === "loading-env-vars" && + onboardingData?.environmentVariables + ) { setState("env-var-sync"); } }, [state, onboardingData?.environmentVariables, onboardingData?.authInvalid]); useEffect(() => { - if (state === "project-selection" && fetcher.data && "success" in fetcher.data && fetcher.data.success && fetcher.state === "idle") { + if ( + state === "project-selection" && + fetcher.data && + "success" in fetcher.data && + fetcher.data.success && + fetcher.state === "idle" + ) { trackOnboarding("vercel onboarding project selected", { vercel_project_name: selectedVercelProject?.name, }); @@ -394,12 +415,20 @@ export function VercelOnboardingModal({ } else if (fetcher.data && "error" in fetcher.data && typeof fetcher.data.error === "string") { setProjectSelectionError(fetcher.data.error); } - }, [state, fetcher.data, fetcher.state, onDataReload, trackOnboarding, selectedVercelProject?.name]); + }, [ + state, + fetcher.data, + fetcher.state, + onDataReload, + trackOnboarding, + selectedVercelProject?.name, + ]); // For marketplace origin, skip env-mapping step useEffect(() => { if (state === "loading-env-mapping" && onboardingData) { - const hasCustomEnvs = (onboardingData.customEnvironments?.length ?? 0) > 0 && hasStagingEnvironment; + const hasCustomEnvs = + (onboardingData.customEnvironments?.length ?? 0) > 0 && hasStagingEnvironment; if (hasCustomEnvs && !fromMarketplaceContext) { setState("env-mapping"); } else { @@ -410,14 +439,13 @@ export function VercelOnboardingModal({ const secretEnvVars = envVars.filter((v) => v.isSecret); const syncableEnvVars = envVars.filter((v) => !v.isSecret); - const enabledEnvVars = syncableEnvVars.filter( - (v) => shouldSyncEnvVarForAnyEnvironment(syncEnvVarsMapping, v.key) + const enabledEnvVars = syncableEnvVars.filter((v) => + shouldSyncEnvVarForAnyEnvironment(syncEnvVarsMapping, v.key) ); const overlappingEnvVarsCount = enabledEnvVars.filter((v) => existingVars[v.key]).length; - const isSubmitting = - navigation.state === "submitting" || navigation.state === "loading"; + const isSubmitting = navigation.state === "submitting" || navigation.state === "loading"; const actionUrl = vercelResourcePath(organizationSlug, projectSlug, environmentSlug); @@ -529,7 +557,6 @@ export function VercelOnboardingModal({ method: "post", action: actionUrl, }); - }, [vercelStagingEnvironment, envMappingFetcher, actionUrl, trackOnboarding]); const handleBuildSettingsNext = useCallback(() => { @@ -541,7 +568,10 @@ export function VercelOnboardingModal({ const formData = new FormData(); formData.append("action", "complete-onboarding"); - formData.append("vercelStagingEnvironment", vercelStagingEnvironment ? JSON.stringify(vercelStagingEnvironment) : ""); + formData.append( + "vercelStagingEnvironment", + vercelStagingEnvironment ? JSON.stringify(vercelStagingEnvironment) : "" + ); formData.append("pullEnvVarsBeforeBuild", JSON.stringify(pullEnvVarsBeforeBuild)); formData.append("atomicBuilds", JSON.stringify(atomicBuilds)); formData.append("discoverEnvVars", JSON.stringify(discoverEnvVars)); @@ -572,24 +602,52 @@ export function VercelOnboardingModal({ github_app_installed: gitHubAppInstallations.length > 0, }); } - }, [vercelStagingEnvironment, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping, nextUrl, fromMarketplaceContext, isGitHubConnectedForOnboarding, completeOnboardingFetcher, actionUrl, trackOnboarding, capture, organizationSlug, projectSlug, gitHubAppInstallations.length]); - - const handleFinishOnboarding = useCallback((e: React.FormEvent) => { - e.preventDefault(); - const form = e.currentTarget; - const formData = new FormData(form); - completeOnboardingFetcher.submit(formData, { - method: "post", - action: actionUrl, - }); - }, [completeOnboardingFetcher, actionUrl]); + }, [ + vercelStagingEnvironment, + pullEnvVarsBeforeBuild, + atomicBuilds, + discoverEnvVars, + syncEnvVarsMapping, + nextUrl, + fromMarketplaceContext, + isGitHubConnectedForOnboarding, + completeOnboardingFetcher, + actionUrl, + trackOnboarding, + capture, + organizationSlug, + projectSlug, + gitHubAppInstallations.length, + ]); + + const handleFinishOnboarding = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + const form = e.currentTarget; + const formData = new FormData(form); + completeOnboardingFetcher.submit(formData, { + method: "post", + action: actionUrl, + }); + }, + [completeOnboardingFetcher, actionUrl] + ); useEffect(() => { - if (completeOnboardingFetcher.data && typeof completeOnboardingFetcher.data === "object" && "success" in completeOnboardingFetcher.data && completeOnboardingFetcher.data.success && completeOnboardingFetcher.state === "idle") { + if ( + completeOnboardingFetcher.data && + typeof completeOnboardingFetcher.data === "object" && + "success" in completeOnboardingFetcher.data && + completeOnboardingFetcher.data.success && + completeOnboardingFetcher.state === "idle" + ) { if (state === "github-connection") { return; } - if ("redirectTo" in completeOnboardingFetcher.data && typeof completeOnboardingFetcher.data.redirectTo === "string") { + if ( + "redirectTo" in completeOnboardingFetcher.data && + typeof completeOnboardingFetcher.data.redirectTo === "string" + ) { const validRedirect = safeRedirectUrl(completeOnboardingFetcher.data.redirectTo); if (validRedirect) { window.location.href = validRedirect; @@ -632,7 +690,13 @@ export function VercelOnboardingModal({ }, [state, organizationSlug, projectSlug]); useEffect(() => { - if (envMappingFetcher.data && typeof envMappingFetcher.data === "object" && "success" in envMappingFetcher.data && envMappingFetcher.data.success && envMappingFetcher.state === "idle") { + if ( + envMappingFetcher.data && + typeof envMappingFetcher.data === "object" && + "success" in envMappingFetcher.data && + envMappingFetcher.data.success && + envMappingFetcher.state === "idle" + ) { setState("loading-env-vars"); } }, [envMappingFetcher.data, envMappingFetcher.state]); @@ -644,9 +708,7 @@ export function VercelOnboardingModal({ if (customEnvironments.length === 1) { selectedEnv = customEnvironments[0]; } else { - const stagingEnv = customEnvironments.find( - (env) => env.slug.toLowerCase() === "staging" - ); + const stagingEnv = customEnvironments.find((env) => env.slug.toLowerCase() === "staging"); selectedEnv = stagingEnv ?? customEnvironments[0]; } @@ -673,14 +735,17 @@ export function VercelOnboardingModal({ if (isLoadingState) { return ( - { - if (!open && !fromMarketplaceContext) { - if (state as string !== "completed") { - trackOnboarding("vercel onboarding abandoned"); + { + if (!open && !fromMarketplaceContext) { + if ((state as string) !== "completed") { + trackOnboarding("vercel onboarding abandoned"); + } + onClose(); } - onClose(); - } - }}> + }} + > e.preventDefault()}>
@@ -704,18 +769,23 @@ export function VercelOnboardingModal({ const disabledEnvSlugsForBuildSettings = hasStagingEnvironment && !vercelStagingEnvironment - ? ({ stg: "Map a custom Vercel environment to Staging to enable this" } as Partial>) + ? ({ stg: "Map a custom Vercel environment to Staging to enable this" } as Partial< + Record + >) : undefined; return ( - { - if (!open && !fromMarketplaceContext) { - if (state !== "completed") { - trackOnboarding("vercel onboarding abandoned"); + { + if (!open && !fromMarketplaceContext) { + if (state !== "completed") { + trackOnboarding("vercel onboarding abandoned"); + } + onClose(); } - onClose(); - } - }}> + }} + > e.preventDefault()}>
@@ -729,8 +799,8 @@ export function VercelOnboardingModal({
Select Vercel Project - Choose which Vercel project to connect with this Trigger.dev project. - Your API keys will be automatically synced to Vercel. + Choose which Vercel project to connect with this Trigger.dev project. Your API keys + will be automatically synced to Vercel. {availableProjects.length === 0 ? ( @@ -763,13 +833,14 @@ export function VercelOnboardingModal({ )} - {projectSelectionError && ( - {projectSelectionError} - )} + {projectSelectionError && {projectSelectionError}} - Once connected, your TRIGGER_SECRET_KEY will be - automatically synced to Vercel for each environment. + Once connected, your{" "} + + TRIGGER_SECRET_KEY + {" "} + will be automatically synced to Vercel for each environment. } cancelButton={ - } @@ -811,11 +879,13 @@ export function VercelOnboardingModal({ Map Vercel Environment to Staging Select which custom Vercel environment should map to Trigger.dev's Staging - environment. Production and Preview environments are mapped automatically. - If you skip this step, the{" "} - TRIGGER_SECRET_KEY{" "} - will not be installed for the staging environment in Vercel. You can configure this later in - project settings. + environment. Production and Preview environments are mapped automatically. If you + skip this step, the{" "} + + TRIGGER_SECRET_KEY + {" "} + will not be installed for the staging environment in Vercel. You can configure this + later in project settings. (() => ({ value }), [value]); @@ -193,11 +193,7 @@ const ClientTabsContent = React.forwardRef< )); diff --git a/apps/webapp/app/components/primitives/CopyTextLink.tsx b/apps/webapp/app/components/primitives/CopyTextLink.tsx index 33818fa607..2c117af9b8 100644 --- a/apps/webapp/app/components/primitives/CopyTextLink.tsx +++ b/apps/webapp/app/components/primitives/CopyTextLink.tsx @@ -16,18 +16,12 @@ export function CopyTextLink({ value, className }: CopyTextLinkProps) { onClick={copy} className={cn( "inline-flex cursor-pointer items-center gap-1 text-xs transition-colors", - copied - ? "text-success" - : "text-text-dimmed hover:text-text-bright", + copied ? "text-success" : "text-text-dimmed hover:text-text-bright", className )} > {copied ? "Copied" : "Copy"} - {copied ? ( - - ) : ( - - )} + {copied ? : } ); } diff --git a/apps/webapp/app/components/primitives/DateTime.tsx b/apps/webapp/app/components/primitives/DateTime.tsx index 4dae92731a..41c51cdd74 100644 --- a/apps/webapp/app/components/primitives/DateTime.tsx +++ b/apps/webapp/app/components/primitives/DateTime.tsx @@ -275,10 +275,10 @@ const DateTimeAccurateInner = ({ return hideDate ? formatTimeOnly(realDate, displayTimeZone, locales, hour12) : realPrevDate - ? isSameDay(realDate, realPrevDate) - ? formatTimeOnly(realDate, displayTimeZone, locales, hour12) - : formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12) - : formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12); + ? isSameDay(realDate, realPrevDate) + ? formatTimeOnly(realDate, displayTimeZone, locales, hour12) + : formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12) + : formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12); }, [realDate, displayTimeZone, locales, hour12, hideDate, previousDate]); if (!showTooltip) diff --git a/apps/webapp/app/components/primitives/Dialog.tsx b/apps/webapp/app/components/primitives/Dialog.tsx index 48fc59cfa7..bf485bbf1a 100644 --- a/apps/webapp/app/components/primitives/Dialog.tsx +++ b/apps/webapp/app/components/primitives/Dialog.tsx @@ -122,5 +122,5 @@ export { DialogTitle, DialogDescription, DialogPortal, - DialogOverlay + DialogOverlay, }; diff --git a/apps/webapp/app/components/primitives/InputNumberStepper.tsx b/apps/webapp/app/components/primitives/InputNumberStepper.tsx index f4aafd5cae..b434bb1d30 100644 --- a/apps/webapp/app/components/primitives/InputNumberStepper.tsx +++ b/apps/webapp/app/components/primitives/InputNumberStepper.tsx @@ -67,7 +67,7 @@ export function InputNumberStepper({ const isMaxDisabled = max !== undefined && !Number.isNaN(numericValue) && numericValue >= max; function clamp(val: number): number { - if (Number.isNaN(val)) return typeof value === "number" ? value : min ?? 0; + if (Number.isNaN(val)) return typeof value === "number" ? value : (min ?? 0); let next = val; if (min !== undefined) next = Math.max(min, next); if (max !== undefined) next = Math.min(max, next); diff --git a/apps/webapp/app/components/primitives/MiddleTruncate.tsx b/apps/webapp/app/components/primitives/MiddleTruncate.tsx index c116205aed..45915c2521 100644 --- a/apps/webapp/app/components/primitives/MiddleTruncate.tsx +++ b/apps/webapp/app/components/primitives/MiddleTruncate.tsx @@ -139,16 +139,9 @@ export function MiddleTruncate({ text, className }: MiddleTruncateProps) { }, [calculateTruncation]); const content = ( - + {/* Hidden span for measuring text width */} -
); } - diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx index 2e4305d423..d9a9556095 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx @@ -80,19 +80,22 @@ const schema = z.object({ if (i === "true") return true; return false; }, z.boolean()), - environmentIds: z.preprocess((i) => { - if (typeof i === "string") return [i]; - - if (Array.isArray(i)) { - const ids = i.filter((v) => typeof v === "string" && v !== ""); - if (ids.length === 0) { - return; + environmentIds: z.preprocess( + (i) => { + if (typeof i === "string") return [i]; + + if (Array.isArray(i)) { + const ids = i.filter((v) => typeof v === "string" && v !== ""); + if (ids.length === 0) { + return; + } + return ids; } - return ids; - } - return; - }, z.array(z.string(), { required_error: "At least one environment is required" })), + return; + }, + z.array(z.string(), { required_error: "At least one environment is required" }) + ), variables: z.preprocess((i) => { if (!Array.isArray(i)) { return []; @@ -222,7 +225,9 @@ export default function Page() { // TODO for no we only support branch-specific env vars for Preview environments // Mostly to keep the UX for setting consistent env-vars across Dev/Staging/Prod easier - const previewBranches = environments.filter((env) => env.type === "PREVIEW" && env.parentEnvironmentId !== null); + const previewBranches = environments.filter( + (env) => env.type === "PREVIEW" && env.parentEnvironmentId !== null + ); const nonBranchEnvironments = environments.filter((env) => env.parentEnvironmentId === null); const selectedEnvironments = environments.filter((env) => selectedEnvironmentIds.has(env.id)); const previewIsSelected = selectedEnvironments.some((env) => env.type === "PREVIEW"); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx index a84e445539..62a14ec22f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx @@ -2,10 +2,7 @@ import { parse } from "@conform-to/zod"; import { BellAlertIcon } from "@heroicons/react/20/solid"; import { type MetaFunction, useFetcher, useRevalidator } from "@remix-run/react"; import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { - IconAlarmSnooze as IconAlarmSnoozeBase, - IconCircleDotted, -} from "@tabler/icons-react"; +import { IconAlarmSnooze as IconAlarmSnoozeBase, IconCircleDotted } from "@tabler/icons-react"; import { ErrorId } from "@trigger.dev/core/v3/isomorphic"; import { isPast } from "date-fns"; import { AnimatePresence, motion } from "framer-motion"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx index 782d2eef8d..e27b11b754 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx @@ -335,9 +335,7 @@ function RateLimitsSection({ /> - {showSelfServe ? ( - Upgrade - ) : null} + {showSelfServe ? Upgrade : null} @@ -557,9 +555,7 @@ function QuotasSection({ Limit Current Source - {showSelfServe ? ( - Upgrade - ) : null} + {showSelfServe ? Upgrade : null} @@ -738,9 +734,7 @@ function FeaturesSection({ Feature Status - {showSelfServe ? ( - Upgrade - ) : null} + {showSelfServe ? Upgrade : null} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx index c913623eba..f0ecd687a4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx @@ -137,7 +137,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const plan = await getCurrentPlan(project.organizationId); const retentionLimitDays = plan?.v3Subscription?.plan?.limits.logRetentionDays.number ?? 30; - const logsClickhouse = await clickhouseFactory.getClickhouseForOrganization(project.organizationId, "logs"); + const logsClickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "logs" + ); const presenter = new LogsListPresenter($replica, logsClickhouse); const listPromise = presenter diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.$modelId/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.$modelId/route.tsx index f4248aa64b..94b4bfc314 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.$modelId/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.$modelId/route.tsx @@ -33,11 +33,7 @@ import { requireUserId } from "~/services/session.server"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useEnvironment } from "~/hooks/useEnvironment"; -import { - EnvironmentParamSchema, - v3ModelComparePath, - v3ModelsPath, -} from "~/utils/pathBuilder"; +import { EnvironmentParamSchema, v3ModelComparePath, v3ModelsPath } from "~/utils/pathBuilder"; import { formatModelPrice, formatTokenCount, @@ -68,7 +64,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { throw new Response("Environment not found", { status: 404 }); } - const clickhouse = await clickhouseFactory.getClickhouseForOrganization(project.organizationId, "standard"); + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "standard" + ); const presenter = new ModelRegistryPresenter(clickhouse); const model = await presenter.getModelDetail(modelId); @@ -101,12 +100,36 @@ function escapeTSQL(value: string): string { return value.replace(/'/g, "''"); } -function bignumberConfig(column: string, opts?: { aggregation?: "sum" | "avg" | "first"; suffix?: string; abbreviate?: boolean }): QueryWidgetConfig { - return { type: "bignumber", column, aggregation: opts?.aggregation ?? "sum", abbreviate: opts?.abbreviate ?? false, suffix: opts?.suffix }; +function bignumberConfig( + column: string, + opts?: { aggregation?: "sum" | "avg" | "first"; suffix?: string; abbreviate?: boolean } +): QueryWidgetConfig { + return { + type: "bignumber", + column, + aggregation: opts?.aggregation ?? "sum", + abbreviate: opts?.abbreviate ?? false, + suffix: opts?.suffix, + }; } -function chartConfig(opts: { chartType: "bar" | "line"; xAxisColumn: string; yAxisColumns: string[]; aggregation?: "sum" | "avg" }): QueryWidgetConfig { - return { type: "chart", chartType: opts.chartType, xAxisColumn: opts.xAxisColumn, yAxisColumns: opts.yAxisColumns, groupByColumn: null, stacked: false, sortByColumn: null, sortDirection: "asc", aggregation: opts.aggregation ?? "sum" }; +function chartConfig(opts: { + chartType: "bar" | "line"; + xAxisColumn: string; + yAxisColumns: string[]; + aggregation?: "sum" | "avg"; +}): QueryWidgetConfig { + return { + type: "chart", + chartType: opts.chartType, + xAxisColumn: opts.xAxisColumn, + yAxisColumns: opts.yAxisColumns, + groupByColumn: null, + stacked: false, + sortByColumn: null, + sortDirection: "asc", + aggregation: opts.aggregation ?? "sum", + }; } type Tab = "overview" | "global" | "usage"; @@ -251,8 +274,8 @@ function CostEstimator({
- Input: {formatModelCost(inputCost)} ({formatTokenCount(inputTokens * numCalls)}{" "} - tokens x {formatModelPrice(inputPrice)}/1M) + Input: {formatModelCost(inputCost)} ({formatTokenCount(inputTokens * numCalls)} tokens + x {formatModelPrice(inputPrice)}/1M)
Output: {formatModelCost(outputCost)} ({formatTokenCount(outputTokens * numCalls)}{" "} @@ -300,17 +323,13 @@ function OverviewTab({ {model.contextWindow && ( Context Window - - {formatTokenCount(model.contextWindow)} tokens - + {formatTokenCount(model.contextWindow)} tokens )} {model.maxOutputTokens && ( Max Output - - {formatTokenCount(model.maxOutputTokens)} tokens - + {formatTokenCount(model.maxOutputTokens)} tokens )} {model.features.length > 0 && ( @@ -342,25 +361,18 @@ function OverviewTab({ Input - - {formatModelPrice(model.inputPrice)} / 1M tokens - + {formatModelPrice(model.inputPrice)} / 1M tokens Output - - {formatModelPrice(model.outputPrice)} / 1M tokens - + {formatModelPrice(model.outputPrice)} / 1M tokens {model.pricingTiers.length > 1 && (

All pricing tiers

{model.pricingTiers.map((tier) => ( -
+
{tier.name} {tier.isDefault && ( @@ -462,7 +474,12 @@ function GlobalMetricsTab({ widgetKey={`${modelName}-ttfc-time`} title="TTFC over time" query={`SELECT timeBucket(), round(quantilesMerge(0.5)(ttfc_quantiles)[1], 0) AS ttfc_p50, round(quantilesMerge(0.9)(ttfc_quantiles)[1], 0) AS ttfc_p90 FROM llm_models WHERE response_model = '${escapeTSQL(modelName)}' GROUP BY timeBucket ORDER BY timeBucket`} - config={chartConfig({ chartType: "line", xAxisColumn: "timebucket", yAxisColumns: ["ttfc_p50", "ttfc_p90"], aggregation: "avg" })} + config={chartConfig({ + chartType: "line", + xAxisColumn: "timebucket", + yAxisColumns: ["ttfc_p50", "ttfc_p90"], + aggregation: "avg", + })} {...widgetProps} />
@@ -546,7 +563,11 @@ function YourUsageTab({ widgetKey={`${modelName}-user-cost-time`} title="Cost over time" query={`SELECT timeBucket(), sum(total_cost) AS cost FROM llm_metrics WHERE response_model = '${escapeTSQL(modelName)}' GROUP BY timeBucket ORDER BY timeBucket`} - config={chartConfig({ chartType: "bar", xAxisColumn: "timebucket", yAxisColumns: ["cost"] })} + config={chartConfig({ + chartType: "bar", + xAxisColumn: "timebucket", + yAxisColumns: ["cost"], + })} {...widgetProps} />
@@ -555,7 +576,11 @@ function YourUsageTab({ widgetKey={`${modelName}-user-tokens-time`} title="Tokens over time" query={`SELECT timeBucket(), sum(input_tokens) AS input_tokens, sum(output_tokens) AS output_tokens FROM llm_metrics WHERE response_model = '${escapeTSQL(modelName)}' GROUP BY timeBucket ORDER BY timeBucket`} - config={chartConfig({ chartType: "bar", xAxisColumn: "timebucket", yAxisColumns: ["input_tokens", "output_tokens"] })} + config={chartConfig({ + chartType: "bar", + xAxisColumn: "timebucket", + yAxisColumns: ["input_tokens", "output_tokens"], + })} {...widgetProps} />
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx index 2b86eb1cd4..328fb9c44e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx @@ -91,7 +91,11 @@ import { requireUserId } from "~/services/session.server"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { EnvironmentParamSchema, v3BuiltInDashboardPath, v3ModelComparePath } from "~/utils/pathBuilder"; +import { + EnvironmentParamSchema, + v3BuiltInDashboardPath, + v3ModelComparePath, +} from "~/utils/pathBuilder"; import { formatModelPrice, formatTokenCount, @@ -126,7 +130,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { throw new Response("Environment not found", { status: 404 }); } - const clickhouse = await clickhouseFactory.getClickhouseForOrganization(project.organizationId, "standard"); + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "standard" + ); const presenter = new ModelRegistryPresenter(clickhouse); const catalog = await presenter.getModelCatalog(); @@ -191,10 +198,7 @@ export function shouldRevalidate({ params.sort(); return params.toString(); }; - if ( - currentUrl.pathname === nextUrl.pathname && - normalize(currentUrl) === normalize(nextUrl) - ) { + if (currentUrl.pathname === nextUrl.pathname && normalize(currentUrl) === normalize(nextUrl)) { return false; } return defaultShouldRevalidate; @@ -1047,7 +1051,7 @@ function DetailYourUsageTab({ projectId, environmentId, scope: "environment" as const, - period: range.from && range.to ? null : range.period ?? "7d", + period: range.from && range.to ? null : (range.period ?? "7d"), from: range.from ?? null, to: range.to ?? null, }; @@ -1241,7 +1245,7 @@ function YourModelsTab({ projectId, environmentId, scope: "environment" as const, - period: from && to ? null : period ?? "7d", + period: from && to ? null : (period ?? "7d"), from, to, }; @@ -1254,7 +1258,11 @@ function YourModelsTab({ widgetKey="your-models-cost-time" title="Cost over time" query={`SELECT timeBucket(), sum(total_cost) AS cost FROM llm_metrics GROUP BY timeBucket ORDER BY timeBucket`} - config={chartConfig({ chartType: "bar", xAxisColumn: "timebucket", yAxisColumns: ["cost"] })} + config={chartConfig({ + chartType: "bar", + xAxisColumn: "timebucket", + yAxisColumns: ["cost"], + })} {...widgetProps} />
@@ -1277,7 +1285,11 @@ function YourModelsTab({ widgetKey="your-models-calls-over-time" title="Calls over time" query={`SELECT timeBucket(), count() AS calls FROM llm_metrics GROUP BY timeBucket ORDER BY timeBucket`} - config={chartConfig({ chartType: "bar", xAxisColumn: "timebucket", yAxisColumns: ["calls"] })} + config={chartConfig({ + chartType: "bar", + xAxisColumn: "timebucket", + yAxisColumns: ["calls"], + })} {...widgetProps} />
@@ -1287,8 +1299,8 @@ function YourModelsTab({ {usage.length === 0 ? (

- No model usage in this environment yet. Models you call from your tasks will appear here - with usage metrics. + No model usage in this environment yet. Models you call from your tasks will appear + here with usage metrics.

diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/ExamplesContent.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/ExamplesContent.tsx index f0c3c1d616..05b4f4d9b6 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/ExamplesContent.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/ExamplesContent.tsx @@ -120,7 +120,8 @@ LIMIT 100`, }, { title: "LLM cost by model (past 7d)", - description: "Total cost, input tokens, and output tokens grouped by model over the last 7 days.", + description: + "Total cost, input tokens, and output tokens grouped by model over the last 7 days.", query: `SELECT response_model, SUM(total_cost) AS total_cost, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/TRQLGuideContent.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/TRQLGuideContent.tsx index 35acfd266f..bfe7bf08f2 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/TRQLGuideContent.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/TRQLGuideContent.tsx @@ -1206,4 +1206,3 @@ ORDER BY run_count DESC`,
); } - diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/TableSchemaContent.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/TableSchemaContent.tsx index 41c17e6d3f..e8ce98f1d4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/TableSchemaContent.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/TableSchemaContent.tsx @@ -77,4 +77,3 @@ export function TableSchemaContent() { ); } - diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index b6afe46be4..9696e5f684 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -315,8 +315,8 @@ export default function Page() { environment.running === environment.concurrencyLimit * environment.burstFactor ? "limit" : environment.running > environment.concurrencyLimit - ? "burst" - : "within"; + ? "burst" + : "within"; const limitClassName = limitStatus === "burst" ? "text-warning" : limitStatus === "limit" ? "text-error" : undefined; @@ -441,9 +441,7 @@ export default function Page() { @@ -676,7 +674,6 @@ export default function Page() { )} - ) : (
@@ -907,7 +904,7 @@ function QueueOverrideConcurrencyButton({ const isLoading = Boolean( navigation.formData?.get("action") === "queue-override" || - navigation.formData?.get("action") === "queue-remove-override" + navigation.formData?.get("action") === "queue-remove-override" ); return ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx index 2d754309a3..d006a853e5 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx @@ -362,88 +362,88 @@ function SetDefaultDialog({
- - Are you sure you want to set {newDefaultRegion.name} as your new default region? - + + Are you sure you want to set {newDefaultRegion.name} as your new default region? + -
-
-
- Current default -
-
- {currentDefaultRegion?.name ?? "–"} -
-
- - {currentDefaultRegion?.cloudProvider ? ( - <> - - {cloudProviderTitle(currentDefaultRegion.cloudProvider)} - - ) : ( - "–" - )} - -
-
- - {currentDefaultRegion?.location ? ( - - ) : null} - {currentDefaultRegion?.description ?? "–"} - +
+
+
+ Current default +
+
+ {currentDefaultRegion?.name ?? "–"} +
+
+ + {currentDefaultRegion?.cloudProvider ? ( + <> + + {cloudProviderTitle(currentDefaultRegion.cloudProvider)} + + ) : ( + "–" + )} + +
+
+ + {currentDefaultRegion?.location ? ( + + ) : null} + {currentDefaultRegion?.description ?? "–"} + +
-
- {/* Middle column with arrow */} -
-
- + {/* Middle column with arrow */} +
+
+ +
-
- {/* Right column */} -
-
- New default -
-
- {newDefaultRegion.name} -
-
- - {newDefaultRegion.cloudProvider ? ( - <> - - {cloudProviderTitle(newDefaultRegion.cloudProvider)} - - ) : ( - "–" - )} - -
-
- - {newDefaultRegion.location ? ( - - ) : null} - {newDefaultRegion.description ?? "–"} - + {/* Right column */} +
+
+ New default +
+
+ {newDefaultRegion.name} +
+
+ + {newDefaultRegion.cloudProvider ? ( + <> + + {cloudProviderTitle(newDefaultRegion.cloudProvider)} + + ) : ( + "–" + )} + +
+
+ + {newDefaultRegion.location ? ( + + ) : null} + {newDefaultRegion.description ?? "–"} + +
-
- - Runs triggered from now on will execute in "{newDefaultRegion.name}", unless you{" "} - override when triggering. - + + Runs triggered from now on will execute in "{newDefaultRegion.name}", unless you{" "} + override when triggering. +
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index de23b935cd..462c5ef293 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -1269,8 +1269,8 @@ function TimelineView({ index === 0 ? "ml-1" : index === tickCount - 1 - ? "-ml-1 -translate-x-full" - : "-translate-x-1/2" + ? "-ml-1 -translate-x-full" + : "-translate-x-1/2" )} > {formatDurationMilliseconds(ms, { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/shouldRevalidateRunsList.ts b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/shouldRevalidateRunsList.ts index 69e0ce6b63..e8c1f19144 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/shouldRevalidateRunsList.ts +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/shouldRevalidateRunsList.ts @@ -26,9 +26,7 @@ export function searchParamsEqualIgnoringBulkInspectorUiState( current: URLSearchParams, next: URLSearchParams ) { - return ( - canonicalRunsListDataSearchParams(current) === canonicalRunsListDataSearchParams(next) - ); + return canonicalRunsListDataSearchParams(current) === canonicalRunsListDataSearchParams(next); } /** True when navigation should show the runs table loading state (excludes bulk-inspector UI toggles). */ diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/useRunsLiveReload.ts b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/useRunsLiveReload.ts index 3ba1c97247..a8d26ac350 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/useRunsLiveReload.ts +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/useRunsLiveReload.ts @@ -91,7 +91,9 @@ function useNewRunsDetection({ isLoading: boolean; }) { const pollTickRef = useRef(0); - const [knownNewestRunMs, setKnownNewestRunMs] = useState(() => maxCreatedAtMs(runs) ?? Date.now()); + const [knownNewestRunMs, setKnownNewestRunMs] = useState( + () => maxCreatedAtMs(runs) ?? Date.now() + ); const [newRunsCount, setNewRunsCount] = useState(0); const shouldPollForNewRuns = hasAnyRuns && !isLoading && newRunsCount < 100; @@ -209,8 +211,7 @@ export function useRunsLiveReload({ const hasActiveRuns = activeRunIdsParam.length > 0; const runsResourcesBasePath = useMemo( - () => - `/resources/orgs/${organizationSlug}/projects/${projectSlug}/env/${environmentSlug}/runs`, + () => `/resources/orgs/${organizationSlug}/projects/${projectSlug}/env/${environmentSlug}/runs`, [organizationSlug, projectSlug, environmentSlug] ); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx index b9bd387159..7d07cb9287 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx @@ -121,8 +121,8 @@ export default function Page() { session.closedAt != null ? "CLOSED" : session.expiresAt != null && new Date(session.expiresAt).getTime() < Date.now() - ? "EXPIRED" - : "ACTIVE"; + ? "EXPIRED" + : "ACTIVE"; const displayId = session.externalId ?? session.friendlyId; const sessionsPath = v3SessionsPath(organization, project, environment); @@ -423,8 +423,8 @@ function RawConversationView({ totalChunks === 0 ? "cursor-not-allowed opacity-50" : copied - ? "text-success hover:cursor-pointer" - : "text-text-dimmed hover:cursor-pointer hover:text-text-bright" + ? "text-success hover:cursor-pointer" + : "text-text-dimmed hover:cursor-pointer hover:text-text-bright" )} > {copied ? : } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx index d66bdb0e0d..e56162731d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx @@ -18,10 +18,7 @@ import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; import { SpinnerWhite } from "~/components/primitives/Spinner"; import { useProject } from "~/hooks/useProject"; -import { - redirectWithErrorMessage, - redirectWithSuccessMessage, -} from "~/models/message.server"; +import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { ProjectSettingsService } from "~/services/projectSettings.server"; import { logger } from "~/services/logger.server"; import { requireUserId } from "~/services/session.server"; @@ -66,7 +63,10 @@ export const action: ActionFunction = async ({ request, params }) => { const userId = await requireUserId(request); const { organizationSlug, projectParam } = params; if (!organizationSlug || !projectParam) { - return json({ errors: { body: "organizationSlug and projectParam are required" } }, { status: 400 }); + return json( + { errors: { body: "organizationSlug and projectParam are required" } }, + { status: 400 } + ); } const formData = await request.formData(); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx index cc85dbb4ac..1777d50e19 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx @@ -7,7 +7,11 @@ import * as Property from "~/components/primitives/PropertyTable"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { useProject } from "~/hooks/useProject"; import { requireUserId } from "~/services/session.server"; -import { EnvironmentParamSchema, v3ProjectSettingsGeneralPath, v3ProjectSettingsIntegrationsPath } from "~/utils/pathBuilder"; +import { + EnvironmentParamSchema, + v3ProjectSettingsGeneralPath, + v3ProjectSettingsIntegrationsPath, +} from "~/utils/pathBuilder"; export const meta: MetaFunction = () => { return [ diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.dashboard/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.dashboard/route.tsx index 65363a3ef1..2d9ac6cd85 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.dashboard/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.dashboard/route.tsx @@ -115,66 +115,66 @@ export default function TasksDashboardPage() {
Tasks overview
- }> - - {(s) => ( - - )} - - - }> - - {(s) => ( - - )} - - - }> - - {(s) => ( - - )} - - + }> + + {(s) => ( + + )} + + + }> + + {(s) => ( + + )} + + + }> + + {(s) => ( + + )} + +
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx index d82ebea7a5..548bf6bf58 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx @@ -149,7 +149,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { from: time.from, to: time.to, }) - .catch(() => ({ data: [], statuses: [] } satisfies TaskActivity)); + .catch(() => ({ data: [], statuses: [] }) satisfies TaskActivity); const pageRaw = parseFiniteInt(url.searchParams.get("page")); const schedulesPage = pageRaw !== undefined && pageRaw > 0 ? pageRaw : 1; @@ -653,8 +653,7 @@ function ScheduleSheet({ // Only show the loading spinner when we actually lack good data β€” // background reloads (e.g. after enable/disable) keep the inspector // visible with its current values until the fresh data arrives. - const isDetailLoading = - isStaleSchedule || (!!openScheduleId && detailFetcher.data === undefined); + const isDetailLoading = isStaleSchedule || (!!openScheduleId && detailFetcher.data === undefined); // Distinct from loading: the loader has resolved and the schedule is // genuinely gone (returned `null`, e.g. deleted externally). const isScheduleMissing = diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx index a6608488e4..1e3d750058 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx @@ -104,7 +104,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { from: time.from, to: time.to, }) - .catch(() => ({ data: [], statuses: [] } satisfies TaskActivity)); + .catch(() => ({ data: [], statuses: [] }) satisfies TaskActivity); const runList = new NextRunListPresenter($replica, clickhouse) .call(project.organizationId, environment.id, { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx index 0e0e0feaba..c5ec45b4ef 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx @@ -310,10 +310,10 @@ export function AIPayloadTabContent({ {isLoading ? "AI is thinking…" : lastResult === "success" - ? "Payload generated" - : lastResult === "error" - ? "Generation failed" - : "AI response"} + ? "Payload generated" + : lastResult === "error" + ? "Generation failed" + : "AI response"}
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 00dcb37f4e..77cd3bf63a 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 @@ -1012,11 +1012,7 @@ function ScheduledTaskForm({ }); return ( -
+
-
- - {task.taskIdentifier} -
-
- - - - setTimestampValue(val)} - granularity="second" - showNowButton - variant="small" - utc - /> - - This is the timestamp of the CRON, it will come through to your run in the payload. - - {timestamp.error} - - - - - setLastTimestampValue(val)} - granularity="second" - showNowButton - showClearButton - variant="small" - utc - /> - - This is the timestamp of the previous run. You can use this in your code to find new - data since the previous run. - - {lastTimestamp.error} - - - - - - The Timestamp and Last timestamp are in UTC so this just changes the timezone string - that comes through in the payload. - - {timezone.error} - - - - setExternalIdValue(e.target.value)} - variant="small" - /> +
+ + {task.taskIdentifier} +
+
+ + + + setTimestampValue(val)} + granularity="second" + showNowButton + variant="small" + utc + /> + + This is the timestamp of the CRON, it will come through to your run in the + payload. + + {timestamp.error} + + + + + setLastTimestampValue(val)} + granularity="second" + showNowButton + showClearButton + variant="small" + utc + /> + + This is the timestamp of the previous run. You can use this in your code to find + new data since the previous run. + + {lastTimestamp.error} + + + + + + The Timestamp and Last timestamp are in UTC so this just changes the timezone + string that comes through in the payload. + + {timezone.error} + + + + setExternalIdValue(e.target.value)} + variant="small" + /> + + Optionally, you can specify your own IDs (like a user ID) and then use it inside + the run function of your task.{" "} + Read the docs. + + {externalId.error} + +
- Optionally, you can specify your own IDs (like a user ID) and then use it inside the - run function of your task.{" "} - Read the docs. + Options enable you to control the execution behavior of your task.{" "} + Read the docs. - {externalId.error} - -
- - Options enable you to control the execution behavior of your task.{" "} - Read the docs. - - - - - Overrides the machine preset. - {machine.error} - - - - - {disableVersionSelection ? ( - Only the latest version is available in the development environment. - ) : ( - Runs task on a specific version. - )} - {version.error} - - {regionItems.length > 1 && ( - - )} - - - {allowArbitraryQueues ? ( - setQueueValue(e.target.value)} - /> - ) : ( + + + {disableVersionSelection ? ( + Only the latest version is available in the development environment. + ) : ( + Runs task on a specific version. + )} + {version.error} + + {regionItems.length > 1 && ( + + + {/* Our Select primitive uses Ariakit under the hood, which treats + value={undefined} as uncontrolled, keeping stale internal state when + switching environments. The key forces a remount so it reinitializes + with the correct defaultValue. */} + + {isDev ? ( + Region is not available in the development environment. + ) : ( + Overrides the region for this run. + )} + {region.error} + )} - Assign run to a specific queue. - {queue.error} - - - - - Add tags to easily filter runs. - {tags.error} - - - - - setMaxAttemptsValue(e.target.value ? parseInt(e.target.value) : undefined) - } - onKeyDown={(e) => { - // only allow entering integers > 1 - if (["-", "+", ".", "e", "E"].includes(e.key)) { - e.preventDefault(); - } - }} - onBlur={(e) => { - const value = parseInt(e.target.value); - if (value < 1 && e.target.value !== "") { - e.target.value = "1"; + + + {allowArbitraryQueues ? ( + setQueueValue(e.target.value)} + /> + ) : ( + + )} + Assign run to a specific queue. + {queue.error} + + + + + Add tags to easily filter runs. + {tags.error} + + + + + setMaxAttemptsValue(e.target.value ? parseInt(e.target.value) : undefined) } - }} - /> - Retries failed runs up to the specified number of attempts. - {maxAttempts.error} - - - - - Overrides the maximum compute time limit for the run. - {maxDurationSeconds.error} - - - - - {idempotencyKey.error} - - Specify an idempotency key to ensure that a task is only triggered once with the - same key. - - - - - - Keys expire after 30 days by default. - - {idempotencyKeyTTLSeconds.error} - - - - - setConcurrencyKeyValue(e.target.value)} - /> - - Limits concurrency by creating a separate queue for each value of the key. - - {concurrencyKey.error} - - - - - Sets the priority of the run. Higher values mean higher priority. - {prioritySeconds.error} - - - - - Expires the run if it hasn't started within the TTL. - {ttlSeconds.error} - -
+ onKeyDown={(e) => { + // only allow entering integers > 1 + if (["-", "+", ".", "e", "E"].includes(e.key)) { + e.preventDefault(); + } + }} + onBlur={(e) => { + const value = parseInt(e.target.value); + if (value < 1 && e.target.value !== "") { + e.target.value = "1"; + } + }} + /> + Retries failed runs up to the specified number of attempts. + {maxAttempts.error} +
+ + + + Overrides the maximum compute time limit for the run. + {maxDurationSeconds.error} + + + + + {idempotencyKey.error} + + Specify an idempotency key to ensure that a task is only triggered once with the + same key. + + + + + + Keys expire after 30 days by default. + + {idempotencyKeyTTLSeconds.error} + + + + + setConcurrencyKeyValue(e.target.value)} + /> + + Limits concurrency by creating a separate queue for each value of the key. + + {concurrencyKey.error} + + + + + Sets the priority of the run. Higher values mean higher priority. + {prioritySeconds.error} + + + + + Expires the run if it hasn't started within the TTL. + {ttlSeconds.error} + +
{/* Toolbar overlay β€” same grid cell, sits above scrolling form. Outer diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx index 54102b86b5..7f762fd030 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx @@ -233,7 +233,6 @@ export default function Page() { )} -
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx index 0eb496880a..74ab939119 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx @@ -51,11 +51,7 @@ import { logger } from "~/services/logger.server"; import { requireUser, requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { extractDomain, faviconUrl as buildFaviconUrl } from "~/utils/favicon"; -import { - OrganizationParamsSchema, - organizationSettingsPath, - rootPath, -} from "~/utils/pathBuilder"; +import { OrganizationParamsSchema, organizationSettingsPath, rootPath } from "~/utils/pathBuilder"; export const meta: MetaFunction = () => { return [ @@ -426,7 +422,7 @@ function LogoForm({ const navigation = useNavigation(); const avatar = navigation.formData - ? avatarFromFormData(navigation.formData) ?? organization.avatar + ? (avatarFromFormData(navigation.formData) ?? organization.avatar) : organization.avatar; const hex = @@ -559,8 +555,7 @@ function LogoForm({ "border-charcoal-775 hover:border-charcoal-600" )} style={{ - borderColor: - avatar.type === "icon" && avatar.name === name ? hex : undefined, + borderColor: avatar.type === "icon" && avatar.name === name ? hex : undefined, }} > - + - + {avatar.type === "icon" && } {defaultAvatarColors.map((color) => (
- +
@@ -649,11 +629,7 @@ function ActiveConnectionState({ Auto-create memberships for first-time SSO sign-ins from your verified domains.
- +
@@ -661,23 +637,20 @@ function ActiveConnectionState({ Default role for JIT provisioned users - Role assigned to new users created via JIT provisioning. Owner is reserved - and cannot be granted automatically. + Role assigned to new users created via JIT provisioning. Owner is reserved and cannot + be granted automatically.
value={draftJitRoleId} setValue={(v) => onChangeJitRole(v === NULL_ROLE_VALUE ? null : v)} - items={[ - { id: NULL_ROLE_VALUE, name: "None", description: "" }, - ...jitRoles, - ]} + items={[{ id: NULL_ROLE_VALUE, name: "None", description: "" }, ...jitRoles]} variant="tertiary/small" dropdownIcon text={(v) => v === NULL_ROLE_VALUE ? "None" - : jitRoles.find((r) => r.id === v)?.name ?? "Select a role" + : (jitRoles.find((r) => r.id === v)?.name ?? "Select a role") } > {(items) => @@ -706,11 +679,7 @@ function ActiveConnectionState({ > Open admin portal -
@@ -719,20 +688,14 @@ function ActiveConnectionState({ ); } -function PortalLinkDialog({ - url, - onClose, -}: { - url: string | null; - onClose: () => void; -}) { +function PortalLinkDialog({ url, onClose }: { url: string | null; onClose: () => void }) { return ( (open ? undefined : onClose())}> Admin portal link - This link is active for 5 minutes β€” copy it and share it with your IT contact via - whatever channel you prefer. + This link is active for 5 minutes β€” copy it and share it with your IT contact via whatever + channel you prefer.
{url ?? ""} @@ -788,13 +751,13 @@ function EnforceConfirmDialog({ Enable SSO enforcement for {orgTitle}? - Once enabled, users whose email domain matches your verified domains will be - redirected to your identity provider to sign in. They will no longer be able to use - magic link, GitHub, or Google via that domain. + Once enabled, users whose email domain matches your verified domains will be redirected to + your identity provider to sign in. They will no longer be able to use magic link, GitHub, + or Google via that domain.

- Users with non-matching emails (e.g. contractors with personal emails) will continue - to use existing methods. + Users with non-matching emails (e.g. contractors with personal emails) will continue to + use existing methods.
- ) + )) ) : triggerButton ? ( cloneElement(triggerButton, { disabled: true, tooltip: noBillingTooltip }) ) : ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx index 1ad36854dc..b80af0568e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx @@ -89,8 +89,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { firstDayOfNextMonth.setUTCDate(1); firstDayOfNextMonth.setUTCHours(0, 0, 0, 0); - const shouldLoadRegions = - !!projectParam && !!environment && environment.type !== "DEVELOPMENT"; + const shouldLoadRegions = !!projectParam && !!environment && environment.type !== "DEVELOPMENT"; const [plan, usage, customDashboards, regions] = await Promise.all([ getCurrentPlan(organization.id), @@ -124,14 +123,14 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const dashboardLimit = typeof metricDashboardsLimitValue === "number" ? metricDashboardsLimitValue - : metricDashboardsLimitValue?.number ?? 3; + : (metricDashboardsLimitValue?.number ?? 3); // Derive widget-per-dashboard limit from plan, fallback to 16 const metricWidgetsLimitValue = plan?.v3Subscription?.plan?.limits?.metricWidgetsPerDashboard; const widgetLimitPerDashboard = typeof metricWidgetsLimitValue === "number" ? metricWidgetsLimitValue - : metricWidgetsLimitValue?.number ?? 16; + : (metricWidgetsLimitValue?.number ?? 16); // Compute widget counts per dashboard from layout JSON const customDashboardsWithWidgetCount = customDashboards.map((d) => { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx index 39cde0cf2d..a7581c9747 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx @@ -361,7 +361,10 @@ export default function Page() { return ( - +
} @@ -452,9 +455,7 @@ export default function Page() { shuffledGoals.indexOf(v) + 1) - )} + value={JSON.stringify(selectedGoals.map((v) => shuffledGoals.indexOf(v) + 1))} /> - + } title="Create an Organization" diff --git a/apps/webapp/app/routes/account.tokens/route.tsx b/apps/webapp/app/routes/account.tokens/route.tsx index d2ef3b5f82..b7919adc89 100644 --- a/apps/webapp/app/routes/account.tokens/route.tsx +++ b/apps/webapp/app/routes/account.tokens/route.tsx @@ -95,9 +95,7 @@ async function loadSystemRolesForUser(userId: string) { // anything else would be a noisy create-time failure (or, with a // permissive fallback, a token bound to a role this org isn't // allowed to issue). - const availableIds = new Set( - (systemRoles ?? []).filter((r) => r.available).map((r) => r.id) - ); + const availableIds = new Set((systemRoles ?? []).filter((r) => r.available).map((r) => r.id)); const roles = allRoles.filter((r) => r.isSystem && availableIds.has(r.id)); return { diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts index 7e5f559a4b..e5c51f9e1a 100644 --- a/apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts +++ b/apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts @@ -87,7 +87,19 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid request body", details: parsed.error.issues }, { status: 400 }); } - const { modelName, matchPattern, startDate, pricingTiers, provider, description, contextWindow, maxOutputTokens, capabilities, isHidden, pricingUnit } = parsed.data; + const { + modelName, + matchPattern, + startDate, + pricingTiers, + provider, + description, + contextWindow, + maxOutputTokens, + capabilities, + isHidden, + pricingUnit, + } = parsed.data; // Validate regex if provided β€” strip (?i) POSIX flag since our registry handles it if (matchPattern) { diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.ts index 6753f37dcf..1d8136ca37 100644 --- a/apps/webapp/app/routes/admin.api.v1.llm-models.ts +++ b/apps/webapp/app/routes/admin.api.v1.llm-models.ts @@ -81,7 +81,20 @@ export async function action({ request }: ActionFunctionArgs) { return json({ error: "Invalid request body", details: parsed.error.issues }, { status: 400 }); } - const { modelName, matchPattern, startDate, source, pricingTiers, provider, description, contextWindow, maxOutputTokens, capabilities, isHidden, pricingUnit } = parsed.data; + const { + modelName, + matchPattern, + startDate, + source, + pricingTiers, + provider, + description, + contextWindow, + maxOutputTokens, + capabilities, + isHidden, + pricingUnit, + } = parsed.data; // Validate regex pattern β€” strip (?i) POSIX flag since our registry handles it try { diff --git a/apps/webapp/app/routes/admin.api.v1.revoked-api-keys.$revokedApiKeyId.ts b/apps/webapp/app/routes/admin.api.v1.revoked-api-keys.$revokedApiKeyId.ts index 847828d981..61eb5a69be 100644 --- a/apps/webapp/app/routes/admin.api.v1.revoked-api-keys.$revokedApiKeyId.ts +++ b/apps/webapp/app/routes/admin.api.v1.revoked-api-keys.$revokedApiKeyId.ts @@ -20,7 +20,10 @@ export async function action({ request, params }: ActionFunctionArgs) { const parsedBody = RequestBodySchema.safeParse(rawBody); if (!parsedBody.success) { - return json({ error: "Invalid request body", issues: parsedBody.error.issues }, { status: 400 }); + return json( + { error: "Invalid request body", issues: parsedBody.error.issues }, + { status: 400 } + ); } const existing = await prisma.revokedApiKey.findFirst({ diff --git a/apps/webapp/app/routes/admin.api.v1.workers.ts b/apps/webapp/app/routes/admin.api.v1.workers.ts index caa36e5217..ffebea96d0 100644 --- a/apps/webapp/app/routes/admin.api.v1.workers.ts +++ b/apps/webapp/app/routes/admin.api.v1.workers.ts @@ -1,8 +1,4 @@ -import { - type ActionFunctionArgs, - type LoaderFunctionArgs, - json, -} from "@remix-run/server-runtime"; +import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; import { type Project, WorkerInstanceGroupType, WorkloadType } from "@trigger.dev/database"; import { z } from "zod"; @@ -174,9 +170,7 @@ export async function action({ request }: ActionFunctionArgs) { } } -async function createWorkerGroup( - options: Parameters[0] -) { +async function createWorkerGroup(options: Parameters[0]) { const service = new WorkerGroupService(); return await service.createWorkerGroup(options); } diff --git a/apps/webapp/app/routes/admin.api.v2.orgs.$organizationId.feature-flags.ts b/apps/webapp/app/routes/admin.api.v2.orgs.$organizationId.feature-flags.ts index 6081febb52..ea0dd757c2 100644 --- a/apps/webapp/app/routes/admin.api.v2.orgs.$organizationId.feature-flags.ts +++ b/apps/webapp/app/routes/admin.api.v2.orgs.$organizationId.feature-flags.ts @@ -5,7 +5,11 @@ import { z } from "zod"; import { prisma } from "~/db.server"; import { requireUser } from "~/services/session.server"; import { flags as getGlobalFlags } from "~/v3/featureFlags.server"; -import { FEATURE_FLAG, validatePartialFeatureFlags, getAllFlagControlTypes } from "~/v3/featureFlags"; +import { + FEATURE_FLAG, + validatePartialFeatureFlags, + getAllFlagControlTypes, +} from "~/v3/featureFlags"; import { featuresForRequest } from "~/features.server"; // Session-auth route for the admin feature flags dialog. diff --git a/apps/webapp/app/routes/admin.back-office._index.tsx b/apps/webapp/app/routes/admin.back-office._index.tsx index e2226aebb4..df4d973760 100644 --- a/apps/webapp/app/routes/admin.back-office._index.tsx +++ b/apps/webapp/app/routes/admin.back-office._index.tsx @@ -4,20 +4,17 @@ import { Header2 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; -export const loader = dashboardLoader( - { authorization: { requireSuper: true } }, - async () => { - return typedjson({}); - } -); +export const loader = dashboardLoader({ authorization: { requireSuper: true } }, async () => { + return typedjson({}); +}); export default function BackOfficeIndex() { return (
Back office - Back-office actions are applied to a single organization. Pick an org from the - Organizations tab to open its detail page. + Back-office actions are applied to a single organization. Pick an org from the Organizations + tab to open its detail page. Pick an organization diff --git a/apps/webapp/app/routes/admin.back-office.orgs.$orgId.tsx b/apps/webapp/app/routes/admin.back-office.orgs.$orgId.tsx index 627c8bd529..23ffeb51bf 100644 --- a/apps/webapp/app/routes/admin.back-office.orgs.$orgId.tsx +++ b/apps/webapp/app/routes/admin.back-office.orgs.$orgId.tsx @@ -1,12 +1,7 @@ import { useNavigation, useSearchParams } from "@remix-run/react"; import { useEffect } from "react"; import { z } from "zod"; -import { - redirect, - typedjson, - useTypedActionData, - useTypedLoaderData, -} from "remix-typedjson"; +import { redirect, typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson"; import { API_RATE_LIMIT_INTENT, API_RATE_LIMIT_SAVED_VALUE, @@ -81,67 +76,61 @@ export const action = dashboardAction( const formData = await request.formData(); const intent = formData.get("intent"); - if (intent === MAX_PROJECTS_INTENT) { - const result = await handleMaxProjectsAction(formData, orgId, user.id); - if (!result.ok) { - return typedjson( - { section: MAX_PROJECTS_SAVED_VALUE, errors: result.errors }, - { status: 400 } + if (intent === MAX_PROJECTS_INTENT) { + const result = await handleMaxProjectsAction(formData, orgId, user.id); + if (!result.ok) { + return typedjson( + { section: MAX_PROJECTS_SAVED_VALUE, errors: result.errors }, + { status: 400 } + ); + } + return redirect( + `/admin/back-office/orgs/${orgId}?${SAVED_QUERY_KEY}=${MAX_PROJECTS_SAVED_VALUE}` ); } - return redirect( - `/admin/back-office/orgs/${orgId}?${SAVED_QUERY_KEY}=${MAX_PROJECTS_SAVED_VALUE}` - ); - } - if (intent === API_RATE_LIMIT_INTENT) { - const result = await handleApiRateLimitAction(formData, orgId, user.id); - if (!result.ok) { - return typedjson( - { section: API_RATE_LIMIT_SAVED_VALUE, errors: result.errors }, - { status: 400 } + if (intent === API_RATE_LIMIT_INTENT) { + const result = await handleApiRateLimitAction(formData, orgId, user.id); + if (!result.ok) { + return typedjson( + { section: API_RATE_LIMIT_SAVED_VALUE, errors: result.errors }, + { status: 400 } + ); + } + return redirect( + `/admin/back-office/orgs/${orgId}?${SAVED_QUERY_KEY}=${API_RATE_LIMIT_SAVED_VALUE}` ); } - return redirect( - `/admin/back-office/orgs/${orgId}?${SAVED_QUERY_KEY}=${API_RATE_LIMIT_SAVED_VALUE}` - ); - } - if (intent === BATCH_RATE_LIMIT_INTENT) { - const result = await handleBatchRateLimitAction(formData, orgId, user.id); - if (!result.ok) { - return typedjson( - { section: BATCH_RATE_LIMIT_SAVED_VALUE, errors: result.errors }, - { status: 400 } + if (intent === BATCH_RATE_LIMIT_INTENT) { + const result = await handleBatchRateLimitAction(formData, orgId, user.id); + if (!result.ok) { + return typedjson( + { section: BATCH_RATE_LIMIT_SAVED_VALUE, errors: result.errors }, + { status: 400 } + ); + } + return redirect( + `/admin/back-office/orgs/${orgId}?${SAVED_QUERY_KEY}=${BATCH_RATE_LIMIT_SAVED_VALUE}` ); } - return redirect( - `/admin/back-office/orgs/${orgId}?${SAVED_QUERY_KEY}=${BATCH_RATE_LIMIT_SAVED_VALUE}` - ); - } - return typedjson( - { section: null, errors: { intent: ["Unknown intent."] } }, - { status: 400 } - ); + return typedjson({ section: null, errors: { intent: ["Unknown intent."] } }, { status: 400 }); } ); export default function BackOfficeOrgPage() { - const { org, apiEffective, batchEffective } = - useTypedLoaderData(); + const { org, apiEffective, batchEffective } = useTypedLoaderData(); const actionData = useTypedActionData(); const navigation = useNavigation(); const submittingIntent = navigation.formData?.get("intent"); - const isSubmittingApi = - navigation.state !== "idle" && submittingIntent === API_RATE_LIMIT_INTENT; + const isSubmittingApi = navigation.state !== "idle" && submittingIntent === API_RATE_LIMIT_INTENT; const isSubmittingBatch = navigation.state !== "idle" && submittingIntent === BATCH_RATE_LIMIT_INTENT; const isSubmittingMaxProjects = navigation.state !== "idle" && submittingIntent === MAX_PROJECTS_INTENT; - const errorSection = - actionData && "section" in actionData ? actionData.section : null; + const errorSection = actionData && "section" in actionData ? actionData.section : null; const errors = actionData && "errors" in actionData ? (actionData.errors as Record) @@ -152,8 +141,7 @@ export default function BackOfficeOrgPage() { // If the action just returned errors for the same section, hide the // "Saved." banner so it doesn't render alongside field errors. Suppressing // here propagates to every read site (auto-dismiss + JSX comparisons). - const savedSection = - errors && errorSection === savedSectionRaw ? null : savedSectionRaw; + const savedSection = errors && errorSection === savedSectionRaw ? null : savedSectionRaw; // Auto-dismiss the "saved" banner after a few seconds. useEffect(() => { diff --git a/apps/webapp/app/routes/admin.back-office.tsx b/apps/webapp/app/routes/admin.back-office.tsx index 3ec9e99b2c..bac76f76ba 100644 --- a/apps/webapp/app/routes/admin.back-office.tsx +++ b/apps/webapp/app/routes/admin.back-office.tsx @@ -2,12 +2,9 @@ import { Outlet } from "@remix-run/react"; import { typedjson } from "remix-typedjson"; import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; -export const loader = dashboardLoader( - { authorization: { requireSuper: true } }, - async () => { - return typedjson({}); - } -); +export const loader = dashboardLoader({ authorization: { requireSuper: true } }, async () => { + return typedjson({}); +}); export default function BackOfficeLayout() { return ( diff --git a/apps/webapp/app/routes/admin.concurrency.tsx b/apps/webapp/app/routes/admin.concurrency.tsx index 630bc100b0..f91f02bd61 100644 --- a/apps/webapp/app/routes/admin.concurrency.tsx +++ b/apps/webapp/app/routes/admin.concurrency.tsx @@ -6,14 +6,11 @@ import { Paragraph } from "~/components/primitives/Paragraph"; import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; import { concurrencyTracker } from "~/v3/services/taskRunConcurrencyTracker.server"; -export const loader = dashboardLoader( - { authorization: { requireSuper: true } }, - async () => { - const deployedConcurrency = await concurrencyTracker.globalConcurrentRunCount(true); - const devConcurrency = await concurrencyTracker.globalConcurrentRunCount(false); - return typedjson({ deployedConcurrency, devConcurrency }); - } -); +export const loader = dashboardLoader({ authorization: { requireSuper: true } }, async () => { + const deployedConcurrency = await concurrencyTracker.globalConcurrentRunCount(true); + const devConcurrency = await concurrencyTracker.globalConcurrentRunCount(false); + return typedjson({ deployedConcurrency, devConcurrency }); +}); export default function AdminDashboardRoute() { const { deployedConcurrency, devConcurrency } = useTypedLoaderData(); diff --git a/apps/webapp/app/routes/admin.data-stores.tsx b/apps/webapp/app/routes/admin.data-stores.tsx index 8dafeb9b2f..5e698c941f 100644 --- a/apps/webapp/app/routes/admin.data-stores.tsx +++ b/apps/webapp/app/routes/admin.data-stores.tsx @@ -337,7 +337,13 @@ function EditButton({ name, organizationIds }: { name: string; organizationIds:
- +
@@ -357,7 +363,12 @@ function EditButton({ name, organizationIds }: { name: string; organizationIds: {fetcher.data?.error &&

{fetcher.data.error}

} - {testResult !== undefined && testResult !== null && (
- Testing: {testResult.modelString} + Testing:{" "} + {testResult.modelString} {testResult.match ? (
@@ -258,9 +256,7 @@ export default function AdminLlmModelsRoute() {
) : ( -
- No match found β€” this model has no pricing data -
+
No match found β€” this model has no pricing data
)}
)} @@ -337,7 +333,10 @@ export default function AdminLlmModelsRoute() { {otherPrices.length > 0 ? ( - p.usageType).join(", ")}> + p.usageType).join(", ")} + > +{otherPrices.length} more ) : ( diff --git a/apps/webapp/app/routes/admin.llm-models.missing.$model.tsx b/apps/webapp/app/routes/admin.llm-models.missing.$model.tsx index 3c63ce09fc..56dc3f4ca4 100644 --- a/apps/webapp/app/routes/admin.llm-models.missing.$model.tsx +++ b/apps/webapp/app/routes/admin.llm-models.missing.$model.tsx @@ -108,7 +108,9 @@ export default function AdminMissingModelDetailRoute() { > {t.key} - {t.min === t.max ? t.min.toLocaleString() : `${t.min.toLocaleString()}-${t.max.toLocaleString()}`} + {t.min === t.max + ? t.min.toLocaleString() + : `${t.min.toLocaleString()}-${t.max.toLocaleString()}`} ))} @@ -123,7 +125,8 @@ export default function AdminMissingModelDetailRoute() { {providerCosts.length > 0 && (
- Provider-reported cost data found in {providerCosts.length} span{providerCosts.length !== 1 ? "s" : ""} + Provider-reported cost data found in {providerCosts.length} span + {providerCosts.length !== 1 ? "s" : ""}
{providerCosts.map((c, i) => ( @@ -143,7 +146,9 @@ export default function AdminMissingModelDetailRoute() {
input: {providerCosts[0].estimatedInputPrice.toExponential(4)} - output: {(providerCosts[0].estimatedOutputPrice ?? 0).toExponential(4)} + + output: {(providerCosts[0].estimatedOutputPrice ?? 0).toExponential(4)} +
Cross-reference with the provider's pricing page before using these estimates. @@ -183,10 +188,7 @@ export default function AdminMissingModelDetailRoute() { } return ( -
+
- + ; }; -const PRICING_UNITS = ["tokens", "characters", "images", "minutes", "requests", "free", "not_findable"]; +const PRICING_UNITS = [ + "tokens", + "characters", + "images", + "minutes", + "requests", + "free", + "not_findable", +]; const COMMON_USAGE_TYPES = [ "input", @@ -495,9 +524,7 @@ function TierEditor({ variant="tertiary/small" onClick={() => { const key = - newUsageType === "__custom" - ? prompt("Usage type name:") ?? "" - : newUsageType; + newUsageType === "__custom" ? (prompt("Usage type name:") ?? "") : newUsageType; if (key) { onChange({ ...tier, prices: { ...tier.prices, [key]: 0 } }); setNewUsageType(""); diff --git a/apps/webapp/app/routes/admin.notifications.tsx b/apps/webapp/app/routes/admin.notifications.tsx index 60b74d104c..d9bab14809 100644 --- a/apps/webapp/app/routes/admin.notifications.tsx +++ b/apps/webapp/app/routes/admin.notifications.tsx @@ -229,8 +229,8 @@ async function handleCreateAction(formData: FormData, userId: string, isPreview: startsAt: isPreview ? new Date().toISOString() : fields.startsAt - ? new Date(fields.startsAt + "Z").toISOString() - : new Date().toISOString(), + ? new Date(fields.startsAt + "Z").toISOString() + : new Date().toISOString(), endsAt: isPreview ? new Date(Date.now() + 60 * 60 * 1000).toISOString() : new Date(fields.endsAt + "Z").toISOString(), @@ -1107,10 +1107,7 @@ function NotificationForm({ > {(items) => items.map((item) => ( - + {item === DISCOVERY_MATCH_NONE ? DISCOVERY_MATCH_LABEL : item} )) diff --git a/apps/webapp/app/routes/admin.orgs.tsx b/apps/webapp/app/routes/admin.orgs.tsx index 8441d4d19d..51cd955232 100644 --- a/apps/webapp/app/routes/admin.orgs.tsx +++ b/apps/webapp/app/routes/admin.orgs.tsx @@ -118,19 +118,13 @@ export default function AdminDashboardRoute() { {org.deletedAt ? "☠️" : ""} - + Open
- typedjson({ user }) +export const loader = dashboardLoader({ authorization: { requireSuper: true } }, async ({ user }) => + typedjson({ user }) ); export default function Page() { diff --git a/apps/webapp/app/routes/api.v1.auth.user-actor-token.ts b/apps/webapp/app/routes/api.v1.auth.user-actor-token.ts index 5031817216..d46743d055 100644 --- a/apps/webapp/app/routes/api.v1.auth.user-actor-token.ts +++ b/apps/webapp/app/routes/api.v1.auth.user-actor-token.ts @@ -49,8 +49,7 @@ export async function action({ request }: ActionFunctionArgs) { if (tokenRole) { return json( { - error: - "Cannot mint a user-actor token from a role-restricted personal access token", + error: "Cannot mint a user-actor token from a role-restricted personal access token", }, { status: 403 } ); diff --git a/apps/webapp/app/routes/api.v1.batches.$batchId.ts b/apps/webapp/app/routes/api.v1.batches.$batchId.ts index 5c0c65baf5..b485dc8605 100644 --- a/apps/webapp/app/routes/api.v1.batches.$batchId.ts +++ b/apps/webapp/app/routes/api.v1.batches.$batchId.ts @@ -33,11 +33,7 @@ export const loader = createLoaderApiRoute( // SDK-issued tokens in the wild β€” a `read:runs` JWT still passes // batch retrieval. Per-id `read:batch:` and type-level // `read:batch` still grant via the first element. - resource: (batch) => - anyResource([ - { type: "batch", id: batch.friendlyId }, - { type: "runs" }, - ]), + resource: (batch) => anyResource([{ type: "batch", id: batch.friendlyId }, { type: "runs" }]), }, }, async ({ resource: batch }) => { diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts index 89aaad4301..78488bfeeb 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.generate-registry-credentials.ts @@ -48,56 +48,64 @@ export async function action({ request, params }: ActionFunctionArgs) { const deploymentService = new DeploymentService(); - return await deploymentService.generateRegistryCredentials(authenticatedEnv, deploymentId).match( - (result) => { - return json( - { - username: result.username, - password: result.password, - expiresAt: result.expiresAt.toISOString(), - repositoryUri: result.repositoryUri, - } satisfies GenerateRegistryCredentialsResponseBody, - { status: 200 } - ); - }, - (error) => { - switch (error.type) { - case "deployment_not_found": - return json({ error: "Deployment not found" }, { status: 404 }); - case "deployment_has_no_image_reference": - logger.error( - "Failed to generate registry credentials: deployment_has_no_image_reference", - { deploymentId } - ); - return json({ error: "Deployment has no image reference" }, { status: 409 }); - case "deployment_is_already_final": - return json( - { error: "Failed to generate registry credentials: deployment_is_already_final" }, - { status: 409 } - ); - case "missing_registry_credentials": - logger.error("Failed to generate registry credentials: missing_registry_credentials", { - deploymentId, - }); - return json({ error: "Missing registry credentials" }, { status: 409 }); - case "registry_not_supported": - logger.error("Failed to generate registry credentials: registry_not_supported", { - deploymentId, - }); - return json({ error: "Registry not supported" }, { status: 409 }); - case "registry_region_not_supported": - logger.error("Failed to generate registry credentials: registry_region_not_supported", { - deploymentId, - }); - return json({ error: "Registry region not supported" }, { status: 409 }); - case "other": - default: - error.type satisfies "other"; - logger.error("Failed to generate registry credentials", { error: error.cause }); - return json({ error: "Internal server error" }, { status: 500 }); + return await deploymentService + .generateRegistryCredentials(authenticatedEnv, deploymentId) + .match( + (result) => { + return json( + { + username: result.username, + password: result.password, + expiresAt: result.expiresAt.toISOString(), + repositoryUri: result.repositoryUri, + } satisfies GenerateRegistryCredentialsResponseBody, + { status: 200 } + ); + }, + (error) => { + switch (error.type) { + case "deployment_not_found": + return json({ error: "Deployment not found" }, { status: 404 }); + case "deployment_has_no_image_reference": + logger.error( + "Failed to generate registry credentials: deployment_has_no_image_reference", + { deploymentId } + ); + return json({ error: "Deployment has no image reference" }, { status: 409 }); + case "deployment_is_already_final": + return json( + { error: "Failed to generate registry credentials: deployment_is_already_final" }, + { status: 409 } + ); + case "missing_registry_credentials": + logger.error( + "Failed to generate registry credentials: missing_registry_credentials", + { + deploymentId, + } + ); + return json({ error: "Missing registry credentials" }, { status: 409 }); + case "registry_not_supported": + logger.error("Failed to generate registry credentials: registry_not_supported", { + deploymentId, + }); + return json({ error: "Registry not supported" }, { status: 409 }); + case "registry_region_not_supported": + logger.error( + "Failed to generate registry credentials: registry_region_not_supported", + { + deploymentId, + } + ); + return json({ error: "Registry region not supported" }, { status: 409 }); + case "other": + default: + error.type satisfies "other"; + logger.error("Failed to generate registry credentials", { error: error.cause }); + return json({ error: "Internal server error" }, { status: 500 }); + } } - } - ); + ); } catch (error) { if (error instanceof Response) throw error; logger.error("Failed to generate registry credentials", { error }); diff --git a/apps/webapp/app/routes/api.v1.idempotencyKeys.$key.reset.ts b/apps/webapp/app/routes/api.v1.idempotencyKeys.$key.reset.ts index f9c5ac0b68..feec6dd655 100644 --- a/apps/webapp/app/routes/api.v1.idempotencyKeys.$key.reset.ts +++ b/apps/webapp/app/routes/api.v1.idempotencyKeys.$key.reset.ts @@ -40,11 +40,13 @@ export const { action } = createActionApiRoute( } logger.error("Failed to reset idempotency key via API", { - error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack } : String(error), + error: + error instanceof Error + ? { name: error.name, message: error.message, stack: error.stack } + : String(error), }); return json({ error: "Internal Server Error" }, { status: 500 }); } - } ); diff --git a/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts b/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts index 6fc85d69cc..0dbde44825 100644 --- a/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts +++ b/apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts @@ -33,18 +33,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); if (!authenticationResult) { - return apiCors( - request, - json({ error: "Invalid or Missing Access Token" }, { status: 401 }) - ); + return apiCors(request, json({ error: "Invalid or Missing Access Token" }, { status: 401 })); } const parsedParams = ParamsSchema.safeParse(params); if (!parsedParams.success) { - return apiCors( - request, - json({ error: "Invalid parameters" }, { status: 400 }) - ); + return apiCors(request, json({ error: "Invalid parameters" }, { status: 400 })); } const { organizationSlug, projectParam } = parsedParams.data; @@ -93,17 +87,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) { projectParam, }); - return apiCors( - request, - json({ error: "Internal server error" }, { status: 500 }) - ); + return apiCors(request, json({ error: "Internal server error" }, { status: 500 })); } if (result.value.type === "not_found") { - return apiCors( - request, - json({ error: "Project not found" }, { status: 404 }) - ); + return apiCors(request, json({ error: "Project not found" }, { status: 404 })); } const { project, integration } = result.value; @@ -150,4 +138,3 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return apiCors(request, json({ error: "Internal Server Error" }, { status: 500 })); } } - diff --git a/apps/webapp/app/routes/api.v1.plain.customer-cards.ts b/apps/webapp/app/routes/api.v1.plain.customer-cards.ts index fe7845c540..ad869c7a4f 100644 --- a/apps/webapp/app/routes/api.v1.plain.customer-cards.ts +++ b/apps/webapp/app/routes/api.v1.plain.customer-cards.ts @@ -16,13 +16,10 @@ const PlainCustomerCardRequestSchema = z.object({ email: z.string().optional(), externalId: z.string().optional(), }) - .refine( - (data) => data.email || data.externalId, - { - message: "Either customer.email or customer.externalId must be provided", - path: ["customer"], - } - ), + .refine((data) => data.email || data.externalId, { + message: "Either customer.email or customer.externalId must be provided", + path: ["customer"], + }), thread: z .object({ id: z.string(), @@ -30,7 +27,10 @@ const PlainCustomerCardRequestSchema = z.object({ .optional(), }); -function sanitizeHeaders(request: Request, skipHeaders?: string[]): Partial> { +function sanitizeHeaders( + request: Request, + skipHeaders?: string[] +): Partial> { const authHeaderName = (env.PLAIN_CUSTOMER_CARDS_HEADERS || "Authorization").toLowerCase(); const defaultSkipHeaders = skipHeaders || [authHeaderName, "cookie"]; const sanitizedHeaders: Partial> = {}; @@ -112,7 +112,6 @@ export async function action({ request }: ActionFunctionArgs) { } try { - const { customer, cardKeys } = parsed.data; const userInclude = { @@ -137,8 +136,8 @@ export async function action({ request }: ActionFunctionArgs) { const where = customer.externalId ? { id: customer.externalId } : customer.email - ? { email: customer.email } - : null; + ? { email: customer.email } + : null; const user = where ? await prisma.user.findFirst({ where, include: userInclude }) : null; @@ -166,7 +165,7 @@ export async function action({ request }: ActionFunctionArgs) { cards.push({ key: accountDetailsKey, - timeToLiveSeconds: 15, + timeToLiveSeconds: 15, components: [ uiComponent.container({ content: [ @@ -283,10 +282,7 @@ export async function action({ request }: ActionFunctionArgs) { } const orgComponents = user.orgMemberships.flatMap( - ( - membership: (typeof user.orgMemberships)[0], - index: number - ) => { + (membership: (typeof user.orgMemberships)[0], index: number) => { const org = membership.organization; const projectCount = org.projects.length; @@ -375,11 +371,9 @@ export async function action({ request }: ActionFunctionArgs) { break; } - const projectComponents = allProjects.slice(0, 10).flatMap( - ( - project: typeof allProjects[0] & { orgSlug: string }, - index: number - ) => { + const projectComponents = allProjects + .slice(0, 10) + .flatMap((project: (typeof allProjects)[0] & { orgSlug: string }, index: number) => { return [ ...(index > 0 ? [uiComponent.divider({ spacingSize: "M" })] : []), uiComponent.text({ @@ -403,8 +397,7 @@ export async function action({ request }: ActionFunctionArgs) { ], }), ]; - } - ); + }); cards.push({ key: "projects", diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts index 7f52d9a523..90bebf0afc 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts @@ -28,7 +28,10 @@ const RequestBodySchema = z.object({ export async function action({ request, params }: ActionFunctionArgs) { try { - const bearer = request.headers.get("Authorization")?.replace(/^Bearer /, "").trim(); + const bearer = request.headers + .get("Authorization") + ?.replace(/^Bearer /, "") + .trim(); const isUat = !!bearer && isUserActorToken(bearer); // A delegated user-actor token authenticates as its user, like a PAT. We diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.repo.snapshot.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.repo.snapshot.ts index 6b4b0518d1..6215b9c8e3 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.repo.snapshot.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.repo.snapshot.ts @@ -27,7 +27,10 @@ const ParamsSchema = z.object({ export async function loader({ request, params }: LoaderFunctionArgs) { try { - const bearer = request.headers.get("Authorization")?.replace(/^Bearer /, "").trim(); + const bearer = request.headers + .get("Authorization") + ?.replace(/^Bearer /, "") + .trim(); let authenticationResult: AuthenticationResult | undefined; if (bearer && isUserActorToken(bearer)) { const claims = await verifyUserActorToken($env.SESSION_SECRET, bearer); diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts index e9977211ec..d99e43c239 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.workers.$tagName.ts @@ -30,7 +30,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // on the user's behalf. Identity-only, same as the PAT path below β€” there's // no ability check on this route, so the cap isn't enforced here (matches // PAT behavior). - const bearer = request.headers.get("Authorization")?.replace(/^Bearer /, "").trim(); + const bearer = request.headers + .get("Authorization") + ?.replace(/^Bearer /, "") + .trim(); let authenticationResult: AuthenticationResult | undefined; if (bearer && isUserActorToken(bearer)) { const claims = await verifyUserActorToken($env.SESSION_SECRET, bearer); @@ -67,63 +70,63 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ); const currentWorker = await findCurrentWorkerFromEnvironment( - { - id: runtimeEnv.id, - type: runtimeEnv.type, - }, - $replica, - params.tagName - ); + { + id: runtimeEnv.id, + type: runtimeEnv.type, + }, + $replica, + params.tagName + ); - if (!currentWorker) { - return json({ error: "Worker not found" }, { status: 404 }); - } + if (!currentWorker) { + return json({ error: "Worker not found" }, { status: 404 }); + } - const tasks = await $replica.backgroundWorkerTask.findMany({ - where: { - workerId: currentWorker.id, - }, - select: { - friendlyId: true, - slug: true, - filePath: true, - triggerSource: true, - createdAt: true, - payloadSchema: true, - }, - orderBy: { - slug: "asc", - }, - }); + const tasks = await $replica.backgroundWorkerTask.findMany({ + where: { + workerId: currentWorker.id, + }, + select: { + friendlyId: true, + slug: true, + filePath: true, + triggerSource: true, + createdAt: true, + payloadSchema: true, + }, + orderBy: { + slug: "asc", + }, + }); - const urls = { - runs: `${$env.APP_ORIGIN}${v3RunsPath( - { slug: runtimeEnv.organization.slug }, - { slug: runtimeEnv.project.slug }, - { slug: runtimeEnv.slug }, - { versions: [currentWorker.version] } - )}`, - }; + const urls = { + runs: `${$env.APP_ORIGIN}${v3RunsPath( + { slug: runtimeEnv.organization.slug }, + { slug: runtimeEnv.project.slug }, + { slug: runtimeEnv.slug }, + { versions: [currentWorker.version] } + )}`, + }; - // Prepare the response object - const response: GetWorkerByTagResponse = { - worker: { - id: currentWorker.friendlyId, - version: currentWorker.version, - engine: currentWorker.engine, - sdkVersion: currentWorker.sdkVersion, - cliVersion: currentWorker.cliVersion, - tasks: tasks.map((task) => ({ - id: task.friendlyId, - slug: task.slug, - filePath: task.filePath, - triggerSource: task.triggerSource, - createdAt: task.createdAt, - payloadSchema: task.payloadSchema, - })), - }, - urls, - }; + // Prepare the response object + const response: GetWorkerByTagResponse = { + worker: { + id: currentWorker.friendlyId, + version: currentWorker.version, + engine: currentWorker.engine, + sdkVersion: currentWorker.sdkVersion, + cliVersion: currentWorker.cliVersion, + tasks: tasks.map((task) => ({ + id: task.friendlyId, + slug: task.slug, + filePath: task.filePath, + triggerSource: task.triggerSource, + createdAt: task.createdAt, + payloadSchema: task.payloadSchema, + })), + }, + urls, + }; return json(response); } catch (error) { diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts index e09bc510d9..7a2bb5b7e6 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.$envSlug.$version.ts @@ -61,21 +61,21 @@ export async function loader({ params, request }: LoaderFunctionArgs) { } return json({ - id: backgroundWorker.friendlyId, - version: backgroundWorker.version, - cliVersion: backgroundWorker.cliVersion, - sdkVersion: backgroundWorker.sdkVersion, - contentHash: backgroundWorker.contentHash, - createdAt: backgroundWorker.createdAt, - updatedAt: backgroundWorker.updatedAt, - tasks: backgroundWorker.tasks.map((task) => ({ - id: task.slug, - exportName: task.exportName ?? "@deprecated", - filePath: task.filePath, - source: task.triggerSource, - retryConfig: task.retryConfig, - queueConfig: task.queueConfig, - })), + id: backgroundWorker.friendlyId, + version: backgroundWorker.version, + cliVersion: backgroundWorker.cliVersion, + sdkVersion: backgroundWorker.sdkVersion, + contentHash: backgroundWorker.contentHash, + createdAt: backgroundWorker.createdAt, + updatedAt: backgroundWorker.updatedAt, + tasks: backgroundWorker.tasks.map((task) => ({ + id: task.slug, + exportName: task.exportName ?? "@deprecated", + filePath: task.filePath, + source: task.triggerSource, + retryConfig: task.retryConfig, + queueConfig: task.queueConfig, + })), files: backgroundWorker.files.map((file) => ({ id: file.friendlyId, filePath: file.filePath, diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.archive.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.archive.ts index ffc82e39b5..1cc565f95e 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.archive.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.branches.archive.ts @@ -65,15 +65,15 @@ export async function action({ request, params }: ActionFunctionArgs) { authenticationResult.type === "organizationAccessToken" ? { id: authenticationResult.result.organizationId } : { - members: { - some: { - userId: authenticationResult.result.userId, + members: { + some: { + userId: authenticationResult.result.userId, + }, }, }, - }, // Dev branches are per-org-member: only the owner may archive their own. ...(authenticationResult.type !== "organizationAccessToken" && - environmentType === "DEVELOPMENT" + environmentType === "DEVELOPMENT" ? { orgMember: { userId: authenticationResult.result.userId } } : {}), project: { @@ -96,7 +96,10 @@ export async function action({ request, params }: ActionFunctionArgs) { activeEnvironments.length > 1 ) { return json( - { error: "Branch name is ambiguous for development environments. Use a personal access token scoped to the branch owner." }, + { + error: + "Branch name is ambiguous for development environments. Use a personal access token scoped to the branch owner.", + }, { status: 409 } ); } diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.environments.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.environments.ts index 5940bcdda0..dd4ce9e2ea 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.environments.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.environments.ts @@ -59,14 +59,16 @@ export const loader = createLoaderPATApiRoute( }, }); - const result: GetProjectEnvironmentsResponseBody = sortEnvironments(environments).map((env) => ({ - id: env.id, - slug: env.slug, - type: env.type, - isBranchableEnvironment: isBranchableEnvironment(env), - branchName: env.branchName, - paused: env.paused, - })); + const result: GetProjectEnvironmentsResponseBody = sortEnvironments(environments).map( + (env) => ({ + id: env.id, + slug: env.slug, + type: env.type, + isBranchableEnvironment: isBranchableEnvironment(env), + branchName: env.branchName, + paused: env.paused, + }) + ); return json(result); } diff --git a/apps/webapp/app/routes/api.v1.prompts.$slug.override.ts b/apps/webapp/app/routes/api.v1.prompts.$slug.override.ts index 2a00ceac15..f31ade052e 100644 --- a/apps/webapp/app/routes/api.v1.prompts.$slug.override.ts +++ b/apps/webapp/app/routes/api.v1.prompts.$slug.override.ts @@ -22,7 +22,10 @@ const UpdateBody = z.object({ commitMessage: z.string().optional(), }); -async function findPrompt(slug: string, authentication: { environment: { projectId: string; id: string } }) { +async function findPrompt( + slug: string, + authentication: { environment: { projectId: string; id: string } } +) { return prisma.prompt.findUnique({ where: { projectId_runtimeEnvironmentId_slug: { diff --git a/apps/webapp/app/routes/api.v1.prompts.$slug.ts b/apps/webapp/app/routes/api.v1.prompts.$slug.ts index 3d8de30c25..316701581e 100644 --- a/apps/webapp/app/routes/api.v1.prompts.$slug.ts +++ b/apps/webapp/app/routes/api.v1.prompts.$slug.ts @@ -52,7 +52,10 @@ export const loader = createLoaderApiRoute( return json({ error: "Prompt not found" }, { status: 404 }); } - const clickhouse = await clickhouseFactory.getClickhouseForOrganization(prompt.project.organizationId, "standard"); + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + prompt.project.organizationId, + "standard" + ); const presenter = new PromptPresenter(clickhouse); const version = await presenter.resolveVersion(prompt.id, { version: searchParams.version, @@ -123,7 +126,10 @@ const { action } = createActionApiRoute( return json({ error: "Prompt not found" }, { status: 404 }); } - const clickhouse = await clickhouseFactory.getClickhouseForOrganization(authentication.environment.organizationId, "standard"); + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + authentication.environment.organizationId, + "standard" + ); const presenter = new PromptPresenter(clickhouse); const version = await presenter.resolveVersion(prompt.id, { version: body.version, @@ -156,21 +162,15 @@ const { action } = createActionApiRoute( export { action }; -function compileTemplate( - template: string, - variables: Record -): string { - let result = template.replace( - /\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, - (_match, key, content) => { - const value = variables[key]; - return value - ? content.replace(/\{\{(\w+)\}\}/g, (_m: string, k: string) => { - return String(variables[k] ?? ""); - }) - : ""; - } - ); +function compileTemplate(template: string, variables: Record): string { + let result = template.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_match, key, content) => { + const value = variables[key]; + return value + ? content.replace(/\{\{(\w+)\}\}/g, (_m: string, k: string) => { + return String(variables[k] ?? ""); + }) + : ""; + }); result = result.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => { const value = variables[key]; diff --git a/apps/webapp/app/routes/api.v1.prompts.$slug.versions.ts b/apps/webapp/app/routes/api.v1.prompts.$slug.versions.ts index 6ef8a014f9..d8c64834aa 100644 --- a/apps/webapp/app/routes/api.v1.prompts.$slug.versions.ts +++ b/apps/webapp/app/routes/api.v1.prompts.$slug.versions.ts @@ -42,7 +42,10 @@ export const loader = createLoaderApiRoute( return json({ error: "Prompt not found" }, { status: 404 }); } - const clickhouse = await clickhouseFactory.getClickhouseForOrganization(prompt.project.organizationId, "standard"); + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + prompt.project.organizationId, + "standard" + ); const presenter = new PromptPresenter(clickhouse); const versions = await presenter.listVersions(prompt.id); diff --git a/apps/webapp/app/routes/api.v1.prompts._index.ts b/apps/webapp/app/routes/api.v1.prompts._index.ts index 8ba10c660d..87939ea879 100644 --- a/apps/webapp/app/routes/api.v1.prompts._index.ts +++ b/apps/webapp/app/routes/api.v1.prompts._index.ts @@ -14,7 +14,10 @@ export const loader = createLoaderApiRoute( }, }, async ({ authentication }) => { - const clickhouse = await clickhouseFactory.getClickhouseForOrganization(authentication.environment.organizationId, "standard"); + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + authentication.environment.organizationId, + "standard" + ); const presenter = new PromptPresenter(clickhouse); const prompts = await presenter.listPrompts( authentication.environment.projectId, diff --git a/apps/webapp/app/routes/api.v1.query.ts b/apps/webapp/app/routes/api.v1.query.ts index 3fb6b04ec7..d729e9e9e2 100644 --- a/apps/webapp/app/routes/api.v1.query.ts +++ b/apps/webapp/app/routes/api.v1.query.ts @@ -1,10 +1,7 @@ import { json } from "@remix-run/server-runtime"; import { QueryError } from "@internal/clickhouse"; import { z } from "zod"; -import { - createActionApiRoute, - everyResource, -} from "~/services/routeBuilders/apiBuilder.server"; +import { createActionApiRoute, everyResource } from "~/services/routeBuilders/apiBuilder.server"; import { executeQuery, type QueryScope } from "~/services/queryService.server"; import { logger } from "~/services/logger.server"; import { rowsToCSV } from "~/utils/dataExport"; diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.events.ts b/apps/webapp/app/routes/api.v1.runs.$runId.events.ts index 42468f6760..43b0340877 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.events.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.events.ts @@ -1,10 +1,7 @@ import { json } from "@remix-run/server-runtime"; import { z } from "zod"; import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server"; -import { - anyResource, - createLoaderApiRoute, -} from "~/services/routeBuilders/apiBuilder.server"; +import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server"; import { getEventRepositoryForStore } from "~/v3/eventRepository/index.server"; diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.metadata.ts b/apps/webapp/app/routes/api.v1.runs.$runId.metadata.ts index 7ec10835c7..58cf572f44 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.metadata.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.metadata.ts @@ -190,9 +190,7 @@ const { action } = createActionApiRoute( // owns the full request shape including parent/root operations, // metadataVersion CAS, batching, validation β€” none of which the // buffer side needs to reimplement. - const [pgError, pgResult] = await tryCatch( - updateMetadataService.call(runId, body, env) - ); + const [pgError, pgResult] = await tryCatch(updateMetadataService.call(runId, body, env)); if (pgError) { if (pgError instanceof ServiceValidationError) { return json({ error: pgError.message }, { status: pgError.status ?? 422 }); @@ -280,13 +278,9 @@ const { action } = createActionApiRoute( routeOperationsToRun( bufferOutcome.parentTaskRunFriendlyId ?? runId, body.parentOperations, - env, - ), - routeOperationsToRun( - bufferOutcome.rootTaskRunFriendlyId ?? runId, - body.rootOperations, - env, + env ), + routeOperationsToRun(bufferOutcome.rootTaskRunFriendlyId ?? runId, body.rootOperations, env), ]); // Wire-shape parity with the PG branch. `UpdateMetadataService.call` diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts b/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts index 061199f33e..5f4c35c155 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts @@ -3,10 +3,7 @@ import { BatchId } from "@trigger.dev/core/v3/isomorphic"; import { z } from "zod"; import { $replica } from "~/db.server"; import { extractAISpanData } from "~/components/runs/v3/ai"; -import { - anyResource, - createLoaderApiRoute, -} from "~/services/routeBuilders/apiBuilder.server"; +import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; import { getEventRepositoryForStore } from "~/v3/eventRepository/index.server"; import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server"; import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.server"; @@ -26,13 +23,13 @@ const ParamsSchema = z.object({ // same 200 contract they'd get for a freshly-triggered run. type ResolvedRun = | { source: "pg"; run: Awaited> & {} } - | { source: "buffer"; run: NonNullable>> }; + | { + source: "buffer"; + run: NonNullable>>; + }; async function findPgRun(runId: string, environmentId: string) { - return runStore.findRun( - { friendlyId: runId, runtimeEnvironmentId: environmentId }, - $replica - ); + return runStore.findRun({ friendlyId: runId, runtimeEnvironmentId: environmentId }, $replica); } export const loader = createLoaderApiRoute( diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts index c3a99fcec4..be717efa35 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tags.ts @@ -85,7 +85,12 @@ export async function action({ request, params }: ActionFunctionArgs) { if (newTags.length === 0) { return json({ message: "No new tags to add" }, { status: 200 }); } - const updated = await runStore.pushTags(taskRun.id, newTags, { runtimeEnvironmentId: env.id }, prisma); + const updated = await runStore.pushTags( + taskRun.id, + newTags, + { runtimeEnvironmentId: env.id }, + prisma + ); // Publish a run-changed record with the NEW tag set so tag feeds reindex // (no-op unless enabled). updatedAt is the read-your-writes watermark. publishChangeRecord({ @@ -114,19 +119,13 @@ export async function action({ request, params }: ActionFunctionArgs) { const newTagsCount = existing ? nonEmptyTags.filter((t) => !existing.includes(t)).length : nonEmptyTags.length; - return json( - { message: `Successfully set ${newTagsCount} new tags.` }, - { status: 200 } - ); + return json({ message: `Successfully set ${newTagsCount} new tags.` }, { status: 200 }); }, // Buffer rejected the append because it would exceed the cap. We // don't know the exact deduped overflow count here (the Lua does), // so report the limit rather than a precise "trying to set N". rejectedResponse: () => - json( - { error: `Runs can only have ${MAX_TAGS_PER_RUN} tags.` }, - { status: 422 } - ), + json({ error: `Runs can only have ${MAX_TAGS_PER_RUN} tags.` }, { status: 422 }), abortSignal: getRequestAbortSignal(), }); diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.trace.ts b/apps/webapp/app/routes/api.v1.runs.$runId.trace.ts index f1aa4d5896..82c356630d 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.trace.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.trace.ts @@ -2,10 +2,7 @@ import { json } from "@remix-run/server-runtime"; import { BatchId } from "@trigger.dev/core/v3/isomorphic"; import { z } from "zod"; import { $replica } from "~/db.server"; -import { - anyResource, - createLoaderApiRoute, -} from "~/services/routeBuilders/apiBuilder.server"; +import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; import { getEventRepositoryForStore } from "~/v3/eventRepository/index.server"; import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server"; import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.server"; @@ -24,13 +21,13 @@ const ParamsSchema = z.object({ // pass-through control case in scripts/mollifier-api-parity.sh). type ResolvedRun = | { source: "pg"; run: Awaited> & {} } - | { source: "buffer"; run: NonNullable>> }; + | { + source: "buffer"; + run: NonNullable>>; + }; async function findPgRun(runId: string, environmentId: string) { - return runStore.findRun( - { friendlyId: runId, runtimeEnvironmentId: environmentId }, - $replica - ); + return runStore.findRun({ friendlyId: runId, runtimeEnvironmentId: environmentId }, $replica); } export const loader = createLoaderApiRoute( diff --git a/apps/webapp/app/routes/api.v1.runs.$runParam.replay.ts b/apps/webapp/app/routes/api.v1.runs.$runParam.replay.ts index 4b238869d3..df684946b1 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runParam.replay.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runParam.replay.ts @@ -125,8 +125,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Run not found" }, { status: 404 }); } - const triggerSource = - sanitizeTriggerSource(request.headers.get("x-trigger-source")) ?? "api"; + const triggerSource = sanitizeTriggerSource(request.headers.get("x-trigger-source")) ?? "api"; const service = new ReplayTaskRunService(); const newRun = await service.call(taskRun, { triggerSource }); diff --git a/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts b/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts index cbdd9807d8..dbc521a742 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runParam.reschedule.ts @@ -91,10 +91,7 @@ export async function action({ request, params }: ActionFunctionArgs) { const snapshotDelayUntil = typeof snapshot.delayUntil === "string" ? snapshot.delayUntil : undefined; if (!snapshotDelayUntil) { - return json( - { error: "Cannot reschedule a run that is not delayed" }, - { status: 422 }, - ); + return json({ error: "Cannot reschedule a run that is not delayed" }, { status: 422 }); } } diff --git a/apps/webapp/app/routes/api.v1.runs.ts b/apps/webapp/app/routes/api.v1.runs.ts index 4cbd689f62..21b7b57857 100644 --- a/apps/webapp/app/routes/api.v1.runs.ts +++ b/apps/webapp/app/routes/api.v1.runs.ts @@ -4,10 +4,7 @@ import { ApiRunListSearchParams, } from "~/presenters/v3/ApiRunListPresenter.server"; import { logger } from "~/services/logger.server"; -import { - anyResource, - createLoaderApiRoute, -} from "~/services/routeBuilders/apiBuilder.server"; +import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; export const loader = createLoaderApiRoute( { diff --git a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts index 8f19194a5d..79ad6091cd 100644 --- a/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts +++ b/apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts @@ -55,7 +55,9 @@ export async function action({ request, params }: ActionFunctionArgs) { // Check if it's a Prisma error if (error instanceof Prisma.PrismaClientKnownRequestError) { return json( - { error: error.code === "P2025" ? "Schedule not found" : clientSafeErrorMessage(error) }, + { + error: error.code === "P2025" ? "Schedule not found" : clientSafeErrorMessage(error), + }, { status: error.code === "P2025" ? 404 : 422 } ); } else { diff --git a/apps/webapp/app/routes/api.v1.sessions.$session.close.ts b/apps/webapp/app/routes/api.v1.sessions.$session.close.ts index 15c2e8dc6b..b52a58c8c3 100644 --- a/apps/webapp/app/routes/api.v1.sessions.$session.close.ts +++ b/apps/webapp/app/routes/api.v1.sessions.$session.close.ts @@ -1,8 +1,5 @@ import { json } from "@remix-run/server-runtime"; -import { - CloseSessionRequestBody, - type RetrieveSessionResponseBody, -} from "@trigger.dev/core/v3"; +import { CloseSessionRequestBody, type RetrieveSessionResponseBody } from "@trigger.dev/core/v3"; import { z } from "zod"; import { $replica, prisma } from "~/db.server"; import { @@ -42,9 +39,7 @@ const { action, loader } = createActionApiRoute( // Idempotent: if already closed, return the current row without clobbering // the original closedAt / closedReason. if (existing.closedAt) { - return json( - await serializeSessionWithFriendlyRunId(existing) - ); + return json(await serializeSessionWithFriendlyRunId(existing)); } // `closedAt: null` on the where clause makes the update conditional at @@ -62,16 +57,12 @@ const { action, loader } = createActionApiRoute( if (count === 0) { const final = await prisma.session.findFirst({ where: { id: existing.id } }); if (!final) return json({ error: "Session not found" }, { status: 404 }); - return json( - await serializeSessionWithFriendlyRunId(final) - ); + return json(await serializeSessionWithFriendlyRunId(final)); } const updated = await prisma.session.findFirst({ where: { id: existing.id } }); if (!updated) return json({ error: "Session not found" }, { status: 404 }); - return json( - await serializeSessionWithFriendlyRunId(updated) - ); + return json(await serializeSessionWithFriendlyRunId(updated)); } ); diff --git a/apps/webapp/app/routes/api.v1.sessions.$session.end-and-continue.ts b/apps/webapp/app/routes/api.v1.sessions.$session.end-and-continue.ts index cc1a6d4f9f..35770928c2 100644 --- a/apps/webapp/app/routes/api.v1.sessions.$session.end-and-continue.ts +++ b/apps/webapp/app/routes/api.v1.sessions.$session.end-and-continue.ts @@ -8,10 +8,7 @@ import { $replica, prisma } from "~/db.server"; import { logger } from "~/services/logger.server"; import { swapSessionRun } from "~/services/realtime/sessionRunManager.server"; import { resolveSessionByIdOrExternalId } from "~/services/realtime/sessions.server"; -import { - anyResource, - createActionApiRoute, -} from "~/services/routeBuilders/apiBuilder.server"; +import { anyResource, createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; import { runStore } from "~/v3/runStore.server"; const ParamsSchema = z.object({ @@ -67,17 +64,11 @@ const { action, loader } = createActionApiRoute( } if (session.closedAt) { - return json( - { error: "Cannot end-and-continue a closed session" }, - { status: 400 } - ); + return json({ error: "Cannot end-and-continue a closed session" }, { status: 400 }); } if (session.expiresAt && session.expiresAt.getTime() < Date.now()) { - return json( - { error: "Cannot end-and-continue an expired session" }, - { status: 400 } - ); + return json({ error: "Cannot end-and-continue an expired session" }, { status: 400 }); } // The wire `callingRunId` is a friendlyId (that's what the agent diff --git a/apps/webapp/app/routes/api.v1.sessions.$session.ts b/apps/webapp/app/routes/api.v1.sessions.$session.ts index 58208de35e..6a92db2793 100644 --- a/apps/webapp/app/routes/api.v1.sessions.$session.ts +++ b/apps/webapp/app/routes/api.v1.sessions.$session.ts @@ -1,8 +1,5 @@ import { json } from "@remix-run/server-runtime"; -import { - type RetrieveSessionResponseBody, - UpdateSessionRequestBody, -} from "@trigger.dev/core/v3"; +import { type RetrieveSessionResponseBody, UpdateSessionRequestBody } from "@trigger.dev/core/v3"; import { Prisma } from "@trigger.dev/database"; import { z } from "zod"; import { $replica, prisma } from "~/db.server"; @@ -44,9 +41,7 @@ export const loader = createLoaderApiRoute( }, }, async ({ resource: session }) => { - return json( - await serializeSessionWithFriendlyRunId(session) - ); + return json(await serializeSessionWithFriendlyRunId(session)); } ); @@ -79,10 +74,7 @@ const { action } = createActionApiRoute( // scope all derive from it. Re-keying a session would orphan its // streams (the chat goes silent) and invalidate the PAT's scope, so // reject any change. Same-value PATCHes stay idempotent. - if ( - body.externalId !== undefined && - body.externalId !== existing.externalId - ) { + if (body.externalId !== undefined && body.externalId !== existing.externalId) { return json( { error: @@ -109,9 +101,7 @@ const { action } = createActionApiRoute( }, }); - return json( - await serializeSessionWithFriendlyRunId(updated) - ); + return json(await serializeSessionWithFriendlyRunId(updated)); } catch (error) { // A duplicate externalId in the same environment violates the // `(runtimeEnvironmentId, externalId)` unique constraint. Surface that diff --git a/apps/webapp/app/routes/api.v1.sessions.$sessionId.snapshot-url.ts b/apps/webapp/app/routes/api.v1.sessions.$sessionId.snapshot-url.ts index 80140f05d2..ba70f08daa 100644 --- a/apps/webapp/app/routes/api.v1.sessions.$sessionId.snapshot-url.ts +++ b/apps/webapp/app/routes/api.v1.sessions.$sessionId.snapshot-url.ts @@ -18,8 +18,10 @@ const routeConfig = { params: ParamsSchema, allowJWT: true, corsStrategy: "all" as const, - findResource: async (params: z.infer, auth: { environment: { id: string } }) => - resolveSessionByIdOrExternalId($replica, auth.environment.id, params.sessionId), + findResource: async ( + params: z.infer, + auth: { environment: { id: string } } + ) => resolveSessionByIdOrExternalId($replica, auth.environment.id, params.sessionId), }; // Authorize against the union of the URL form, friendlyId, and externalId β€” @@ -75,19 +77,20 @@ export const loader = createLoaderApiRoute( }, }, async ({ authentication, resource: session }) => { - if (!session) { - return json({ error: "Session not found" }, { status: 404 }); - } + if (!session) { + return json({ error: "Session not found" }, { status: 404 }); + } - const signed = await generatePresignedUrl( - authentication.environment.project.externalRef, - authentication.environment.slug, - chatSnapshotStorageKey(session), - "GET" - ); - if (!signed.success) { - return json({ error: `Failed to generate presigned URL: ${signed.error}` }, { status: 500 }); - } + const signed = await generatePresignedUrl( + authentication.environment.project.externalRef, + authentication.environment.slug, + chatSnapshotStorageKey(session), + "GET" + ); + if (!signed.success) { + return json({ error: `Failed to generate presigned URL: ${signed.error}` }, { status: 500 }); + } - return json({ presignedUrl: signed.url }); -}); + return json({ presignedUrl: signed.url }); + } +); diff --git a/apps/webapp/app/routes/api.v1.sessions.ts b/apps/webapp/app/routes/api.v1.sessions.ts index ec8c171fc2..1a10e6c6f0 100644 --- a/apps/webapp/app/routes/api.v1.sessions.ts +++ b/apps/webapp/app/routes/api.v1.sessions.ts @@ -158,10 +158,7 @@ const { action } = createActionApiRoute( // via the JWT ability's wildcard branches. action: "write", resource: (_params, _searchParams, _headers, body) => - anyResource([ - { type: "tasks", id: body.taskIdentifier }, - { type: "sessions" }, - ]), + anyResource([{ type: "tasks", id: body.taskIdentifier }, { type: "sessions" }]), }, corsStrategy: "all", }, @@ -280,10 +277,7 @@ const { action } = createActionApiRoute( // the externalId; otherwise the friendlyId. Mirrors the // canonical addressing key used server-side. const addressingKey = session.externalId ?? session.friendlyId; - const publicAccessToken = await mintSessionToken( - authentication.environment, - addressingKey - ); + const publicAccessToken = await mintSessionToken(authentication.environment, addressingKey); const sessionItem: SessionItem = { ...serializeSession(session), diff --git a/apps/webapp/app/routes/api.v1.tasks.$taskId.trigger.ts b/apps/webapp/app/routes/api.v1.tasks.$taskId.trigger.ts index eb9e5d974e..2b3f308dfa 100644 --- a/apps/webapp/app/routes/api.v1.tasks.$taskId.trigger.ts +++ b/apps/webapp/app/routes/api.v1.tasks.$taskId.trigger.ts @@ -128,7 +128,9 @@ const { action, loader } = createActionApiRoute( realtimeStreamsVersion: determineRealtimeStreamsVersion( realtimeStreamsVersion ?? undefined ), - triggerSource: isFromWorker ? "sdk" : sanitizeTriggerSource(triggerSourceHeader) ?? "api", + triggerSource: isFromWorker + ? "sdk" + : (sanitizeTriggerSource(triggerSourceHeader) ?? "api"), triggerAction: "trigger", }, engineVersion ?? undefined diff --git a/apps/webapp/app/routes/api.v1.tasks.batch.ts b/apps/webapp/app/routes/api.v1.tasks.batch.ts index 16b3ef1606..2c33e1b4a0 100644 --- a/apps/webapp/app/routes/api.v1.tasks.batch.ts +++ b/apps/webapp/app/routes/api.v1.tasks.batch.ts @@ -7,10 +7,7 @@ import { import { env } from "~/env.server"; import { AuthenticatedEnvironment, getOneTimeUseToken } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; -import { - createActionApiRoute, - everyResource, -} from "~/services/routeBuilders/apiBuilder.server"; +import { createActionApiRoute, everyResource } from "~/services/routeBuilders/apiBuilder.server"; import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { @@ -125,7 +122,7 @@ const { action, loader } = createActionApiRoute( realtimeStreamsVersion: determineRealtimeStreamsVersion( realtimeStreamsVersion ?? undefined ), - triggerSource: isFromWorker ? "sdk" : sanitizeTriggerSource(triggerSourceHeader) ?? "api", + triggerSource: isFromWorker ? "sdk" : (sanitizeTriggerSource(triggerSourceHeader) ?? "api"), triggerAction: "trigger", }); diff --git a/apps/webapp/app/routes/api.v2.batches.$batchId.ts b/apps/webapp/app/routes/api.v2.batches.$batchId.ts index a4a3027cfb..deb3b78822 100644 --- a/apps/webapp/app/routes/api.v2.batches.$batchId.ts +++ b/apps/webapp/app/routes/api.v2.batches.$batchId.ts @@ -27,11 +27,7 @@ export const loader = createLoaderApiRoute( action: "read", // See sibling note in api.v1.batches.$batchId.ts β€” `{type: "runs"}` // preserves pre-RBAC `read:runs` superScope access for batch reads. - resource: (batch) => - anyResource([ - { type: "batch", id: batch.friendlyId }, - { type: "runs" }, - ]), + resource: (batch) => anyResource([{ type: "batch", id: batch.friendlyId }, { type: "runs" }]), }, }, async ({ resource: batch }) => { diff --git a/apps/webapp/app/routes/api.v2.tasks.batch.ts b/apps/webapp/app/routes/api.v2.tasks.batch.ts index 974b1ed296..4845bcf473 100644 --- a/apps/webapp/app/routes/api.v2.tasks.batch.ts +++ b/apps/webapp/app/routes/api.v2.tasks.batch.ts @@ -9,10 +9,7 @@ import { env } from "~/env.server"; import { RunEngineBatchTriggerService } from "~/runEngine/services/batchTrigger.server"; import { AuthenticatedEnvironment, getOneTimeUseToken } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; -import { - createActionApiRoute, - everyResource, -} from "~/services/routeBuilders/apiBuilder.server"; +import { createActionApiRoute, everyResource } from "~/services/routeBuilders/apiBuilder.server"; import { handleRequestIdempotency, saveRequestIdempotency, @@ -140,7 +137,7 @@ const { action, loader } = createActionApiRoute( realtimeStreamsVersion: determineRealtimeStreamsVersion( realtimeStreamsVersion ?? undefined ), - triggerSource: isFromWorker ? "sdk" : sanitizeTriggerSource(triggerSourceHeader) ?? "api", + triggerSource: isFromWorker ? "sdk" : (sanitizeTriggerSource(triggerSourceHeader) ?? "api"), triggerAction: "trigger", }); diff --git a/apps/webapp/app/routes/api.v3.batches.ts b/apps/webapp/app/routes/api.v3.batches.ts index 8787ae4701..a42706432d 100644 --- a/apps/webapp/app/routes/api.v3.batches.ts +++ b/apps/webapp/app/routes/api.v3.batches.ts @@ -132,7 +132,7 @@ const { action, loader } = createActionApiRoute( realtimeStreamsVersion: determineRealtimeStreamsVersion( realtimeStreamsVersion ?? undefined ), - triggerSource: isFromWorker ? "sdk" : sanitizeTriggerSource(triggerSourceHeader) ?? "api", + triggerSource: isFromWorker ? "sdk" : (sanitizeTriggerSource(triggerSourceHeader) ?? "api"), }); const $responseHeaders = await responseHeaders( diff --git a/apps/webapp/app/routes/api.v3.runs.$runId.ts b/apps/webapp/app/routes/api.v3.runs.$runId.ts index 00ea710258..5debce7035 100644 --- a/apps/webapp/app/routes/api.v3.runs.$runId.ts +++ b/apps/webapp/app/routes/api.v3.runs.$runId.ts @@ -1,10 +1,7 @@ import { json } from "@remix-run/server-runtime"; import { z } from "zod"; import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server"; -import { - anyResource, - createLoaderApiRoute, -} from "~/services/routeBuilders/apiBuilder.server"; +import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; const ParamsSchema = z.object({ runId: z.string(), diff --git a/apps/webapp/app/routes/auth.google.callback.tsx b/apps/webapp/app/routes/auth.google.callback.tsx index 932186b511..593b9d40fb 100644 --- a/apps/webapp/app/routes/auth.google.callback.tsx +++ b/apps/webapp/app/routes/auth.google.callback.tsx @@ -95,4 +95,3 @@ export let loader: LoaderFunction = async ({ request }) => { return redirect(redirectTo, { headers }); }; - diff --git a/apps/webapp/app/routes/auth.google.ts b/apps/webapp/app/routes/auth.google.ts index 125fd8ec9d..95fb4ff7b5 100644 --- a/apps/webapp/app/routes/auth.google.ts +++ b/apps/webapp/app/routes/auth.google.ts @@ -35,4 +35,3 @@ export const redirectCookie = createCookie("google-redirect-to", { sameSite: "lax", secure: env.NODE_ENV === "production", }); - diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index 2430af764e..ab84f9c34f 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -242,7 +242,10 @@ export default function Page() { return ( - + { +async function cancelRunsInline(runFriendlyIds: string[], environmentId: string): Promise { const runIds = runFriendlyIds.map((fid) => RunId.toId(fid)); const runs = await runStore.findRuns( diff --git a/apps/webapp/app/routes/engine.v1.dev.presence.ts b/apps/webapp/app/routes/engine.v1.dev.presence.ts index 23970ac960..bda6466cec 100644 --- a/apps/webapp/app/routes/engine.v1.dev.presence.ts +++ b/apps/webapp/app/routes/engine.v1.dev.presence.ts @@ -12,7 +12,6 @@ export const loader = createSSELoader({ handler: async ({ id, controller, debug, request }) => { const authentication = await authenticateApiRequestWithFailure(request); - if (!authentication.ok) { throw json({ error: "Invalid or Missing API key" }, { status: 401 }); } diff --git a/apps/webapp/app/routes/engine.v1.worker-actions.connect.ts b/apps/webapp/app/routes/engine.v1.worker-actions.connect.ts index 247984d454..713a344d05 100644 --- a/apps/webapp/app/routes/engine.v1.worker-actions.connect.ts +++ b/apps/webapp/app/routes/engine.v1.worker-actions.connect.ts @@ -1,5 +1,8 @@ import { json, TypedResponse } from "@remix-run/server-runtime"; -import { WorkerApiConnectRequestBody, WorkerApiConnectResponseBody } from "@trigger.dev/core/v3/workers"; +import { + WorkerApiConnectRequestBody, + WorkerApiConnectResponseBody, +} from "@trigger.dev/core/v3/workers"; import { createActionWorkerApiRoute } from "~/services/routeBuilders/apiBuilder.server"; export const action = createActionWorkerApiRoute( diff --git a/apps/webapp/app/routes/engine.v1.worker-actions.heartbeat.ts b/apps/webapp/app/routes/engine.v1.worker-actions.heartbeat.ts index 111cab6011..92a6645a71 100644 --- a/apps/webapp/app/routes/engine.v1.worker-actions.heartbeat.ts +++ b/apps/webapp/app/routes/engine.v1.worker-actions.heartbeat.ts @@ -1,5 +1,8 @@ import { json, TypedResponse } from "@remix-run/server-runtime"; -import { WorkerApiHeartbeatResponseBody, WorkerApiHeartbeatRequestBody } from "@trigger.dev/core/v3/workers"; +import { + WorkerApiHeartbeatResponseBody, + WorkerApiHeartbeatRequestBody, +} from "@trigger.dev/core/v3/workers"; import { createActionWorkerApiRoute } from "~/services/routeBuilders/apiBuilder.server"; export const action = createActionWorkerApiRoute( diff --git a/apps/webapp/app/routes/invites.tsx b/apps/webapp/app/routes/invites.tsx index 7b234d4129..aa28eff0e3 100644 --- a/apps/webapp/app/routes/invites.tsx +++ b/apps/webapp/app/routes/invites.tsx @@ -101,7 +101,10 @@ export default function Page() { return ( - +
} diff --git a/apps/webapp/app/routes/login._index/route.tsx b/apps/webapp/app/routes/login._index/route.tsx index 9c7df4f912..ca418e2d7f 100644 --- a/apps/webapp/app/routes/login._index/route.tsx +++ b/apps/webapp/app/routes/login._index/route.tsx @@ -204,7 +204,11 @@ export default function LoginPage() {
{data.lastAuthMethod === "email" && } = { }, domain_policy: { heading: "SSO required", - body: - "Trigger.dev couldn't send a magic link because your organization requires single sign-on. Continue to your identity provider.", + body: "Trigger.dev couldn't send a magic link because your organization requires single sign-on. Continue to your identity provider.", }, oauth_blocked: { heading: "SSO required", - body: - "You can't use that provider to sign in — your organization requires SSO. Continue with your identity provider.", + body: "You can't use that provider to sign in — your organization requires SSO. Continue with your identity provider.", }, expired: { heading: "Login attempt timed out", @@ -84,7 +82,9 @@ export async function loader({ request }: LoaderFunctionArgs) { reason, email, redirectTo, - errorMessage: errorCode ? (ERROR_MESSAGES[errorCode] ?? "We couldn't complete sign-in. Try again.") : null, + errorMessage: errorCode + ? (ERROR_MESSAGES[errorCode] ?? "We couldn't complete sign-in. Try again.") + : null, }); } diff --git a/apps/webapp/app/routes/magic.tsx b/apps/webapp/app/routes/magic.tsx index c309f27dbc..eccd889f1b 100644 --- a/apps/webapp/app/routes/magic.tsx +++ b/apps/webapp/app/routes/magic.tsx @@ -38,9 +38,7 @@ export async function loader({ request }: LoaderFunctionArgs) { const session = await getSession(request.headers.get("cookie")); session.flash("auth:error", { message: - thrown instanceof Error - ? thrown.message - : "Your magic link is invalid or has expired.", + thrown instanceof Error ? thrown.message : "Your magic link is invalid or has expired.", }); return redirect("/login/magic", { headers: { "Set-Cookie": await commitSession(session) }, diff --git a/apps/webapp/app/routes/otel.v1.metrics.ts b/apps/webapp/app/routes/otel.v1.metrics.ts index 803dc3da26..a73442ff51 100644 --- a/apps/webapp/app/routes/otel.v1.metrics.ts +++ b/apps/webapp/app/routes/otel.v1.metrics.ts @@ -13,9 +13,7 @@ export async function action({ request }: ActionFunctionArgs) { const exporter = await otlpExporter; const body = await request.json(); - const exportResponse = await exporter.exportMetrics( - body as ExportMetricsServiceRequest - ); + const exportResponse = await exporter.exportMetrics(body as ExportMetricsServiceRequest); return json(exportResponse, { status: 200 }); } else if (contentType.startsWith("application/x-protobuf")) { diff --git a/apps/webapp/app/routes/realtime.v1.batches.$batchId.ts b/apps/webapp/app/routes/realtime.v1.batches.$batchId.ts index add50434d4..07906292d4 100644 --- a/apps/webapp/app/routes/realtime.v1.batches.$batchId.ts +++ b/apps/webapp/app/routes/realtime.v1.batches.$batchId.ts @@ -25,11 +25,7 @@ export const loader = createLoaderApiRoute( action: "read", // See sibling note in api.v1.batches.$batchId.ts — `{type: "runs"}` // preserves pre-RBAC `read:runs` superScope access for batch reads. - resource: (batch) => - anyResource([ - { type: "batch", id: batch.friendlyId }, - { type: "runs" }, - ]), + resource: (batch) => anyResource([{ type: "batch", id: batch.friendlyId }, { type: "runs" }]), }, }, async ({ authentication, request, resource: batchRun, apiVersion }) => { diff --git a/apps/webapp/app/routes/realtime.v1.runs.$runId.ts b/apps/webapp/app/routes/realtime.v1.runs.$runId.ts index f2268989bd..297190bc44 100644 --- a/apps/webapp/app/routes/realtime.v1.runs.$runId.ts +++ b/apps/webapp/app/routes/realtime.v1.runs.$runId.ts @@ -3,10 +3,7 @@ import { z } from "zod"; import { $replica } from "~/db.server"; import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server"; import { resolveRealtimeStreamClient } from "~/services/realtime/resolveRealtimeStreamClient.server"; -import { - anyResource, - createLoaderApiRoute, -} from "~/services/routeBuilders/apiBuilder.server"; +import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; import { runStore } from "~/v3/runStore.server"; const ParamsSchema = z.object({ diff --git a/apps/webapp/app/routes/realtime.v1.runs.ts b/apps/webapp/app/routes/realtime.v1.runs.ts index 2e3617800f..c0600b392b 100644 --- a/apps/webapp/app/routes/realtime.v1.runs.ts +++ b/apps/webapp/app/routes/realtime.v1.runs.ts @@ -1,10 +1,7 @@ import { z } from "zod"; import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server"; import { resolveRealtimeStreamClient } from "~/services/realtime/resolveRealtimeStreamClient.server"; -import { - anyResource, - createLoaderApiRoute, -} from "~/services/routeBuilders/apiBuilder.server"; +import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; const SearchParamsSchema = z.object({ tags: z diff --git a/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts b/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts index 0bdcceb714..ddd4ab4ac5 100644 --- a/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts +++ b/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts @@ -15,10 +15,7 @@ import { drainSessionStreamWaitpoints, releaseSessionStreamPart, } from "~/services/sessionStreamWaitpointCache.server"; -import { - anyResource, - createActionApiRoute, -} from "~/services/routeBuilders/apiBuilder.server"; +import { anyResource, createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; import { engine } from "~/v3/runEngine.server"; import { ServiceValidationError } from "~/v3/services/common.server"; @@ -82,17 +79,11 @@ const { action, loader } = createActionApiRoute( } if (session.closedAt) { - return json( - { ok: false, error: "Cannot append to a closed session" }, - { status: 400 } - ); + return json({ ok: false, error: "Cannot append to a closed session" }, { status: 400 }); } if (session.expiresAt && session.expiresAt.getTime() < Date.now()) { - return json( - { ok: false, error: "Cannot append to an expired session" }, - { status: 400 } - ); + return json({ ok: false, error: "Cannot append to an expired session" }, { status: 400 }); } // `.out` is the agent→client channel. Only PRIVATE (secret key) auth — diff --git a/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.ts b/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.ts index ab64621750..e846e851be 100644 --- a/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.ts +++ b/apps/webapp/app/routes/realtime.v1.sessions.$session.$io.ts @@ -98,10 +98,7 @@ const loader = createLoaderApiRoute( allowJWT: true, corsStrategy: "all", findResource: async (params, auth) => { - const row = await resolveSessionWithWriterFallback( - auth.environment.id, - params.session - ); + const row = await resolveSessionWithWriterFallback(auth.environment.id, params.session); if (!row && isSessionFriendlyIdForm(params.session)) { return undefined; // 404 — opaque friendlyId must reference a real row } diff --git a/apps/webapp/app/routes/realtime.v1.streams.$runId.$streamId.ts b/apps/webapp/app/routes/realtime.v1.streams.$runId.$streamId.ts index 81784f9bc3..19e56e5e0a 100644 --- a/apps/webapp/app/routes/realtime.v1.streams.$runId.$streamId.ts +++ b/apps/webapp/app/routes/realtime.v1.streams.$runId.$streamId.ts @@ -3,10 +3,7 @@ import { z } from "zod"; import { $replica } from "~/db.server"; import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server"; import { getRealtimeStreamInstance } from "~/services/realtime/v1StreamsGlobal.server"; -import { - anyResource, - createLoaderApiRoute, -} from "~/services/routeBuilders/apiBuilder.server"; +import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; import { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { runStore } from "~/v3/runStore.server"; @@ -155,9 +152,15 @@ export const loader = createLoaderApiRoute( { run } ); - return realtimeStream.streamResponse(request, run.friendlyId, params.streamId, getRequestAbortSignal(), { - lastEventId, - timeoutInSeconds, - }); + return realtimeStream.streamResponse( + request, + run.friendlyId, + params.streamId, + getRequestAbortSignal(), + { + lastEventId, + timeoutInSeconds, + } + ); } ); diff --git a/apps/webapp/app/routes/realtime.v1.streams.$runId.$target.$streamId.append.ts b/apps/webapp/app/routes/realtime.v1.streams.$runId.$target.$streamId.append.ts index 7cb813a6de..12400e8d0b 100644 --- a/apps/webapp/app/routes/realtime.v1.streams.$runId.$target.$streamId.append.ts +++ b/apps/webapp/app/routes/realtime.v1.streams.$runId.$target.$streamId.append.ts @@ -59,8 +59,8 @@ const { action } = createActionApiRoute( params.target === "self" ? run.friendlyId : params.target === "parent" - ? run.parentTaskRun?.friendlyId - : run.rootTaskRun?.friendlyId; + ? run.parentTaskRun?.friendlyId + : run.rootTaskRun?.friendlyId; if (!targetId) { return new Response("Target not found", { status: 404 }); diff --git a/apps/webapp/app/routes/realtime.v1.streams.$runId.$target.$streamId.ts b/apps/webapp/app/routes/realtime.v1.streams.$runId.$target.$streamId.ts index c71ad48d12..01a2b550d4 100644 --- a/apps/webapp/app/routes/realtime.v1.streams.$runId.$target.$streamId.ts +++ b/apps/webapp/app/routes/realtime.v1.streams.$runId.$target.$streamId.ts @@ -54,8 +54,8 @@ const { action } = createActionApiRoute( params.target === "self" ? run : params.target === "parent" - ? run.parentTaskRun - : run.rootTaskRun; + ? run.parentTaskRun + : run.rootTaskRun; if (!targetRun?.friendlyId) { return new Response("Target not found", { status: 404 }); @@ -191,8 +191,8 @@ const loader = createLoaderApiRoute( params.target === "self" ? run : params.target === "parent" - ? run.parentTaskRun - : run.rootTaskRun; + ? run.parentTaskRun + : run.rootTaskRun; if (!targetRun?.friendlyId) { return new Response("Target not found", { status: 404 }); @@ -209,11 +209,9 @@ const loader = createLoaderApiRoute( const clientId = request.headers.get("X-Client-Id") || "default"; const streamVersion = request.headers.get("X-Stream-Version") || "v1"; - const realtimeStream = getRealtimeStreamInstance( - authentication.environment, - streamVersion, - { run: { streamBasinName: targetRun.streamBasinName ?? null } } - ); + const realtimeStream = getRealtimeStreamInstance(authentication.environment, streamVersion, { + run: { streamBasinName: targetRun.streamBasinName ?? null }, + }); const lastChunkIndex = await realtimeStream.getLastChunkIndex( targetId, diff --git a/apps/webapp/app/routes/resources.account.mfa.setup/useMfaSetup.ts b/apps/webapp/app/routes/resources.account.mfa.setup/useMfaSetup.ts index 0a69d44617..8e1aad9ae1 100644 --- a/apps/webapp/app/routes/resources.account.mfa.setup/useMfaSetup.ts +++ b/apps/webapp/app/routes/resources.account.mfa.setup/useMfaSetup.ts @@ -2,7 +2,7 @@ import { useReducer, useEffect } from "react"; import { useTypedFetcher } from "remix-typedjson"; import { action } from "./route"; -export type MfaPhase = 'idle' | 'enabling' | 'validating' | 'showing-recovery' | 'disabling'; +export type MfaPhase = "idle" | "enabling" | "validating" | "showing-recovery" | "disabling"; export interface MfaState { phase: MfaPhase; @@ -14,150 +14,150 @@ export interface MfaState { recoveryCodes?: string[]; error?: string; isSubmitting: boolean; - disableMethod: 'totp' | 'recovery'; + disableMethod: "totp" | "recovery"; } -export type MfaAction = - | { type: 'ENABLE_MFA' } - | { type: 'SETUP_DATA_RECEIVED'; setupData: { secret: string; otpAuthUrl: string } } - | { type: 'CANCEL_SETUP' } - | { type: 'VALIDATE_TOTP'; code: string } - | { type: 'VALIDATION_SUCCESS'; recoveryCodes: string[] } - | { type: 'VALIDATION_FAILED'; error: string; setupData: { secret: string; otpAuthUrl: string } } - | { type: 'RECOVERY_CODES_SAVED' } - | { type: 'OPEN_DISABLE_DIALOG' } - | { type: 'DISABLE_MFA' } - | { type: 'DISABLE_SUCCESS' } - | { type: 'DISABLE_FAILED'; error: string } - | { type: 'CANCEL_DISABLE' } - | { type: 'SET_DISABLE_METHOD'; method: 'totp' | 'recovery' } - | { type: 'SET_ERROR'; error: string } - | { type: 'CLEAR_ERROR' } - | { type: 'SET_SUBMITTING'; isSubmitting: boolean }; +export type MfaAction = + | { type: "ENABLE_MFA" } + | { type: "SETUP_DATA_RECEIVED"; setupData: { secret: string; otpAuthUrl: string } } + | { type: "CANCEL_SETUP" } + | { type: "VALIDATE_TOTP"; code: string } + | { type: "VALIDATION_SUCCESS"; recoveryCodes: string[] } + | { type: "VALIDATION_FAILED"; error: string; setupData: { secret: string; otpAuthUrl: string } } + | { type: "RECOVERY_CODES_SAVED" } + | { type: "OPEN_DISABLE_DIALOG" } + | { type: "DISABLE_MFA" } + | { type: "DISABLE_SUCCESS" } + | { type: "DISABLE_FAILED"; error: string } + | { type: "CANCEL_DISABLE" } + | { type: "SET_DISABLE_METHOD"; method: "totp" | "recovery" } + | { type: "SET_ERROR"; error: string } + | { type: "CLEAR_ERROR" } + | { type: "SET_SUBMITTING"; isSubmitting: boolean }; function mfaReducer(state: MfaState, action: MfaAction): MfaState { switch (action.type) { - case 'ENABLE_MFA': + case "ENABLE_MFA": return { ...state, - phase: 'enabling', + phase: "enabling", isSubmitting: true, error: undefined, }; - case 'SETUP_DATA_RECEIVED': + case "SETUP_DATA_RECEIVED": return { ...state, - phase: 'enabling', + phase: "enabling", setupData: action.setupData, error: undefined, isSubmitting: false, }; - case 'CANCEL_SETUP': + case "CANCEL_SETUP": return { ...state, - phase: 'idle', + phase: "idle", setupData: undefined, error: undefined, isSubmitting: false, }; - case 'VALIDATE_TOTP': + case "VALIDATE_TOTP": return { ...state, - phase: 'validating', + phase: "validating", isSubmitting: true, error: undefined, }; - case 'VALIDATION_SUCCESS': + case "VALIDATION_SUCCESS": return { ...state, - phase: 'showing-recovery', + phase: "showing-recovery", recoveryCodes: action.recoveryCodes, isSubmitting: false, isEnabled: true, error: undefined, }; - case 'VALIDATION_FAILED': + case "VALIDATION_FAILED": return { ...state, - phase: 'enabling', + phase: "enabling", setupData: action.setupData, error: action.error, isSubmitting: false, }; - case 'RECOVERY_CODES_SAVED': + case "RECOVERY_CODES_SAVED": return { ...state, - phase: 'idle', + phase: "idle", setupData: undefined, recoveryCodes: undefined, isSubmitting: false, }; - case 'OPEN_DISABLE_DIALOG': + case "OPEN_DISABLE_DIALOG": return { ...state, - phase: 'disabling', + phase: "disabling", error: undefined, isSubmitting: false, }; - case 'DISABLE_MFA': + case "DISABLE_MFA": return { ...state, isSubmitting: true, error: undefined, }; - case 'DISABLE_SUCCESS': + case "DISABLE_SUCCESS": return { ...state, - phase: 'idle', + phase: "idle", isEnabled: false, error: undefined, isSubmitting: false, }; - case 'DISABLE_FAILED': + case "DISABLE_FAILED": return { ...state, error: action.error, isSubmitting: false, }; - case 'CANCEL_DISABLE': + case "CANCEL_DISABLE": return { ...state, - phase: 'idle', + phase: "idle", error: undefined, isSubmitting: false, }; - case 'SET_DISABLE_METHOD': + case "SET_DISABLE_METHOD": return { ...state, disableMethod: action.method, error: undefined, }; - case 'SET_ERROR': + case "SET_ERROR": return { ...state, error: action.error, }; - case 'CLEAR_ERROR': + case "CLEAR_ERROR": return { ...state, error: undefined, }; - case 'SET_SUBMITTING': + case "SET_SUBMITTING": return { ...state, isSubmitting: action.isSubmitting, @@ -170,55 +170,55 @@ function mfaReducer(state: MfaState, action: MfaAction): MfaState { export function useMfaSetup(initialIsEnabled: boolean) { const fetcher = useTypedFetcher(); - + const [state, dispatch] = useReducer(mfaReducer, { - phase: 'idle', + phase: "idle", isEnabled: initialIsEnabled, isSubmitting: false, - disableMethod: 'totp', + disableMethod: "totp", }); // Handle fetcher responses useEffect(() => { if (fetcher.data) { const { data } = fetcher; - + switch (data.action) { - case 'enable-mfa': - dispatch({ - type: 'SETUP_DATA_RECEIVED', - setupData: { secret: data.secret, otpAuthUrl: data.otpAuthUrl } + case "enable-mfa": + dispatch({ + type: "SETUP_DATA_RECEIVED", + setupData: { secret: data.secret, otpAuthUrl: data.otpAuthUrl }, }); break; - - case 'validate-totp': + + case "validate-totp": if (data.success) { - dispatch({ - type: 'VALIDATION_SUCCESS', - recoveryCodes: data.recoveryCodes || [] + dispatch({ + type: "VALIDATION_SUCCESS", + recoveryCodes: data.recoveryCodes || [], }); } else { - dispatch({ - type: 'VALIDATION_FAILED', - error: data.error || 'Invalid code', - setupData: { secret: data.secret!, otpAuthUrl: data.otpAuthUrl! } + dispatch({ + type: "VALIDATION_FAILED", + error: data.error || "Invalid code", + setupData: { secret: data.secret!, otpAuthUrl: data.otpAuthUrl! }, }); } break; - - case 'disable-mfa': + + case "disable-mfa": if (data.success) { - dispatch({ type: 'DISABLE_SUCCESS' }); + dispatch({ type: "DISABLE_SUCCESS" }); } else { - dispatch({ - type: 'DISABLE_FAILED', - error: data.error || 'Failed to disable MFA' + dispatch({ + type: "DISABLE_FAILED", + error: data.error || "Failed to disable MFA", }); } break; - - case 'cancel-totp': - dispatch({ type: 'CANCEL_SETUP' }); + + case "cancel-totp": + dispatch({ type: "CANCEL_SETUP" }); break; } } @@ -226,68 +226,65 @@ export function useMfaSetup(initialIsEnabled: boolean) { // Handle submitting state useEffect(() => { - dispatch({ type: 'SET_SUBMITTING', isSubmitting: fetcher.state === 'submitting' }); + dispatch({ type: "SET_SUBMITTING", isSubmitting: fetcher.state === "submitting" }); }, [fetcher.state]); const actions = { enableMfa: () => { - dispatch({ type: 'ENABLE_MFA' }); + dispatch({ type: "ENABLE_MFA" }); fetcher.submit( - { action: 'enable-mfa' }, - { method: 'POST', action: '/resources/account/mfa/setup' } + { action: "enable-mfa" }, + { method: "POST", action: "/resources/account/mfa/setup" } ); }, cancelSetup: () => { - dispatch({ type: 'CANCEL_SETUP' }); + dispatch({ type: "CANCEL_SETUP" }); fetcher.submit( - { action: 'cancel-totp' }, - { method: 'POST', action: '/resources/account/mfa/setup' } + { action: "cancel-totp" }, + { method: "POST", action: "/resources/account/mfa/setup" } ); }, validateTotp: (code: string) => { - dispatch({ type: 'VALIDATE_TOTP', code }); + dispatch({ type: "VALIDATE_TOTP", code }); fetcher.submit( - { action: 'validate-totp', totpCode: code }, - { method: 'POST', action: '/resources/account/mfa/setup' } + { action: "validate-totp", totpCode: code }, + { method: "POST", action: "/resources/account/mfa/setup" } ); }, saveRecoveryCodes: () => { - dispatch({ type: 'RECOVERY_CODES_SAVED' }); + dispatch({ type: "RECOVERY_CODES_SAVED" }); fetcher.submit( - { action: 'saved-recovery-codes' }, - { method: 'POST', action: '/resources/account/mfa/setup' } + { action: "saved-recovery-codes" }, + { method: "POST", action: "/resources/account/mfa/setup" } ); }, openDisableDialog: () => { - dispatch({ type: 'OPEN_DISABLE_DIALOG' }); + dispatch({ type: "OPEN_DISABLE_DIALOG" }); }, disableMfa: (totpCode?: string, recoveryCode?: string) => { - dispatch({ type: 'DISABLE_MFA' }); - const formData: Record = { action: 'disable-mfa' }; + dispatch({ type: "DISABLE_MFA" }); + const formData: Record = { action: "disable-mfa" }; if (totpCode) formData.totpCode = totpCode; if (recoveryCode) formData.recoveryCode = recoveryCode; - - fetcher.submit( - formData, - { method: 'POST', action: '/resources/account/mfa/setup' } - ); + + fetcher.submit(formData, { method: "POST", action: "/resources/account/mfa/setup" }); }, cancelDisable: () => { - dispatch({ type: 'CANCEL_DISABLE' }); + dispatch({ type: "CANCEL_DISABLE" }); }, - setDisableMethod: (method: 'totp' | 'recovery') => { - dispatch({ type: 'SET_DISABLE_METHOD', method }); + setDisableMethod: (method: "totp" | "recovery") => { + dispatch({ type: "SET_DISABLE_METHOD", method }); }, clearError: () => { - dispatch({ type: 'CLEAR_ERROR' }); + dispatch({ type: "CLEAR_ERROR" }); }, }; @@ -295,8 +292,10 @@ export function useMfaSetup(initialIsEnabled: boolean) { state, actions, // Computed properties for easier access - isQrDialogOpen: (state.phase === 'enabling' && !!state.setupData) || (state.phase === 'showing-recovery' && !!state.recoveryCodes), + isQrDialogOpen: + (state.phase === "enabling" && !!state.setupData) || + (state.phase === "showing-recovery" && !!state.recoveryCodes), isRecoveryDialogOpen: false, // Recovery is now handled within the setup dialog - isDisableDialogOpen: state.phase === 'disabling', + isDisableDialogOpen: state.phase === "disabling", }; -} \ No newline at end of file +} diff --git a/apps/webapp/app/routes/resources.branches.archive.tsx b/apps/webapp/app/routes/resources.branches.archive.tsx index 3b7f178da1..105a21644f 100644 --- a/apps/webapp/app/routes/resources.branches.archive.tsx +++ b/apps/webapp/app/routes/resources.branches.archive.tsx @@ -46,8 +46,8 @@ export async function action({ request }: ActionFunctionArgs) { if (result.success) { return redirectWithSuccessMessage( - result.branch.type === "DEVELOPMENT" ? - branchesDevPath(result.organization, result.project, result.branch) + result.branch.type === "DEVELOPMENT" + ? branchesDevPath(result.organization, result.project, result.branch) : branchesPath(result.organization, result.project, result.branch), request, `Branch "${result.branch.branchName}" archived` diff --git a/apps/webapp/app/routes/resources.feedback.ts b/apps/webapp/app/routes/resources.feedback.ts index 62b13b518b..3a7e5bd34b 100644 --- a/apps/webapp/app/routes/resources.feedback.ts +++ b/apps/webapp/app/routes/resources.feedback.ts @@ -49,10 +49,7 @@ export const feedbackTypes = { labelTypeId: "lt_01KS54WBRYKE6DY369KPK2SS4W", threadTitle: "Web app: HIPAA BAA request", }, -} as const satisfies Record< - string, - { label: string; labelTypeId?: string; threadTitle: string } ->; +} as const satisfies Record; export type FeedbackType = keyof typeof feedbackTypes; 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 f55307c7c3..501b4a8ad3 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 @@ -42,12 +42,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); const { organizationSlug } = OrganizationParamsSchema.parse(params); - const canViewLogsPage = user.admin || user.isImpersonating || await hasLogsPageAccess( - user.id, - user.admin, - user.isImpersonating, - organizationSlug - ); + const canViewLogsPage = + user.admin || + user.isImpersonating || + (await hasLogsPageAccess(user.id, user.admin, user.isImpersonating, organizationSlug)); return typedjson({ canViewLogsPage }); }; 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 66f5aae80c..c2a4bf59ff 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 @@ -55,7 +55,14 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = user.id; const { organizationSlug, projectParam } = EnvironmentParamSchema.parse(params); - if (!(await canAccessDashboardAgent({ userId, isAdmin: user.admin, isImpersonating: user.isImpersonating, organizationSlug }))) { + if ( + !(await canAccessDashboardAgent({ + userId, + isAdmin: user.admin, + isImpersonating: user.isImpersonating, + organizationSlug, + })) + ) { return json({ error: "Not found" }, { status: 404 }); } @@ -83,7 +90,14 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const userId = user.id; const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); - if (!(await canAccessDashboardAgent({ userId, isAdmin: user.admin, isImpersonating: user.isImpersonating, organizationSlug }))) { + if ( + !(await canAccessDashboardAgent({ + userId, + isAdmin: user.admin, + isImpersonating: user.isImpersonating, + organizationSlug, + })) + ) { return json({ error: "Not found" }, { status: 404 }); } @@ -186,7 +200,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { // id). The transport falls back here to re-establish a session for an // existing chat (e.g. after its token expired), so verify ownership before // issuing one — a client-supplied chatId must belong to the caller. - if (!(await chatExists(dashboardAgentDb, { chatId, userId, organizationId: project.organizationId }))) { + if ( + !(await chatExists(dashboardAgentDb, { + chatId, + userId, + organizationId: project.organizationId, + })) + ) { return json({ error: "Chat not found" }, { status: 404 }); } let clientData: Record | undefined; @@ -215,7 +235,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { } // Only mint a session token for a chat the caller owns, so a client-supplied // chatId can't be used to get a token for someone else's session. - if (!(await chatExists(dashboardAgentDb, { chatId, userId, organizationId: project.organizationId }))) { + if ( + !(await chatExists(dashboardAgentDb, { + chatId, + userId, + organizationId: project.organizationId, + })) + ) { return json({ error: "Chat not found" }, { status: 404 }); } return json({ token: await mintDashboardAgentToken(chatId) }); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx index 51bbded593..ee3f56147d 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx @@ -6,10 +6,7 @@ import { prisma } from "~/db.server"; import { QueryWidgetConfig } from "~/components/metrics/QueryWidget"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; -import { - DashboardLayout, - LayoutItem, -} from "~/presenters/v3/MetricDashboardPresenter.server"; +import { DashboardLayout, LayoutItem } from "~/presenters/v3/MetricDashboardPresenter.server"; import { getCurrentPlan } from "~/services/platform.v3.server"; import { requireUserId } from "~/services/session.server"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; @@ -145,10 +142,9 @@ async function saveDashboardLayout( }); if (result.count === 0) { - throw new Response( - "Dashboard was modified by another request. Please refresh and try again.", - { status: 409 } - ); + throw new Response("Dashboard was modified by another request. Please refresh and try again.", { + status: 409, + }); } } diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.create.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.create.tsx index 1e33b3b681..ef5ed4fa1e 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.create.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.create.tsx @@ -29,8 +29,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { }), ]); - const metricDashboardsLimitValue = (plan?.v3Subscription?.plan?.limits as any) - ?.metricDashboards; + const metricDashboardsLimitValue = (plan?.v3Subscription?.plan?.limits as any)?.metricDashboards; const dashboardLimit = typeof metricDashboardsLimitValue === "number" ? metricDashboardsLimitValue 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 be4fdba7fe..418cee805c 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 @@ -44,7 +44,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const [traceId, spanId, , startTime] = parts; - const logsClickhouse = await clickhouseFactory.getClickhouseForOrganization(project.organizationId, "logs"); + const logsClickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "logs" + ); const presenter = new LogDetailPresenter($replica, logsClickhouse); let result; 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 38b7dd390f..8e918ec5f2 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 @@ -4,7 +4,11 @@ import { requireUser, requireUserId } from "~/services/session.server"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; -import { LogsListPresenter, type LogLevel, LogsListOptionsSchema } from "~/presenters/v3/LogsListPresenter.server"; +import { + LogsListPresenter, + type LogLevel, + LogsListOptionsSchema, +} from "~/presenters/v3/LogsListPresenter.server"; import { $replica } from "~/db.server"; import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; import { getCurrentPlan } from "~/services/platform.v3.server"; @@ -69,7 +73,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { retentionLimitDays, }) as any; // Validated by LogsListOptionsSchema at runtime - const logsClickhouse = await clickhouseFactory.getClickhouseForOrganization(project.organizationId, "logs"); + const logsClickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "logs" + ); const presenter = new LogsListPresenter($replica, logsClickhouse); const result = await presenter.call(project.organizationId, environment.id, options); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx index 826618277a..9485f5ff4c 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.action.tsx @@ -117,7 +117,12 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const tags = [ `chat:${chatId}`, "playground:true", - ...(tagsStr ? tagsStr.split(",").map((t) => t.trim()).filter(Boolean) : []), + ...(tagsStr + ? tagsStr + .split(",") + .map((t) => t.trim()) + .filter(Boolean) + : []), ].slice(0, 5); const triggerConfig = { @@ -262,12 +267,11 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { }); if (existing?.title === "New conversation") { - const firstUserMsg = messagesData.find( - (m: any) => m.role === "user" - ) as Record | undefined; + const firstUserMsg = messagesData.find((m: any) => m.role === "user") as + | Record + | undefined; const firstText = - firstUserMsg?.parts?.find((p: any) => p.type === "text")?.text ?? - firstUserMsg?.content; + firstUserMsg?.parts?.find((p: any) => p.type === "text")?.text ?? firstUserMsg?.content; if (firstText && typeof firstText === "string") { titleUpdate = { title: firstText.length > 60 ? firstText.slice(0, 60) + "..." : firstText, diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.realtime.v1.sessions.$session.$io.append.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.realtime.v1.sessions.$session.$io.append.ts index 038e053a8b..0d15865931 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.realtime.v1.sessions.$session.$io.append.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.realtime.v1.sessions.$session.$io.append.ts @@ -56,27 +56,17 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ ok: false, error: "Request body too large" }, { status: 413 }); } - const session = await resolveSessionByIdOrExternalId( - $replica, - environment.id, - sessionParam - ); + const session = await resolveSessionByIdOrExternalId($replica, environment.id, sessionParam); if (!session) { return json({ ok: false, error: "Session not found" }, { status: 404 }); } if (session.closedAt) { - return json( - { ok: false, error: "Cannot append to a closed session" }, - { status: 400 } - ); + return json({ ok: false, error: "Cannot append to a closed session" }, { status: 400 }); } if (session.expiresAt && session.expiresAt.getTime() < Date.now()) { - return json( - { ok: false, error: "Cannot append to an expired session" }, - { status: 400 } - ); + return json({ ok: false, error: "Cannot append to an expired session" }, { status: 400 }); } const realtimeStream = getRealtimeStreamInstance(environment, "v2", { session }); @@ -117,10 +107,7 @@ export async function action({ request, params }: ActionFunctionArgs) { if (appendError) { if (appendError instanceof ServiceValidationError) { - return json( - { ok: false, error: appendError.message }, - { status: appendError.status ?? 422 } - ); + return json({ ok: false, error: appendError.message }, { status: appendError.status ?? 422 }); } return json({ ok: false, error: appendError.message }, { status: 500 }); } diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.realtime.v1.sessions.$session.$io.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.realtime.v1.sessions.$session.$io.ts index e54bfe3575..317bd38a7d 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.realtime.v1.sessions.$session.$io.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.realtime.v1.sessions.$session.$io.ts @@ -41,11 +41,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return new Response("Environment not found", { status: 404 }); } - const session = await resolveSessionByIdOrExternalId( - $replica, - environment.id, - sessionParam - ); + const session = await resolveSessionByIdOrExternalId($replica, environment.id, sessionParam); if (!session) { return new Response("Session not found", { status: 404 }); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug.generations.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug.generations.ts index e468438cdc..9897514032 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug.generations.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug.generations.ts @@ -22,8 +22,9 @@ export type GenerationsResponse = { export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { projectParam, organizationSlug, envParam, promptSlug } = - EnvironmentParamSchema.extend({ promptSlug: z.string() }).parse(params); + const { projectParam, organizationSlug, envParam, promptSlug } = EnvironmentParamSchema.extend({ + promptSlug: z.string(), + }).parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) throw new Response("Project not found", { status: 404 }); @@ -59,7 +60,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const operations = url.searchParams.getAll("operations").filter(Boolean); const providers = url.searchParams.getAll("providers").filter(Boolean); - const clickhouse = await clickhouseFactory.getClickhouseForOrganization(project.organizationId, "standard"); + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "standard" + ); const presenter = new PromptPresenter(clickhouse); const result = await presenter.listGenerations({ environmentId: environment.id, diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.realtime.v1.sessions.$sessionId.$io.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.realtime.v1.sessions.$sessionId.$io.ts index 3a0dfca568..c001d66d50 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.realtime.v1.sessions.$sessionId.$io.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.realtime.v1.sessions.$sessionId.$io.ts @@ -66,11 +66,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return new Response("Run not found", { status: 404 }); } - const session = await resolveSessionByIdOrExternalId( - $replica, - environment.id, - sessionId - ); + const session = await resolveSessionByIdOrExternalId($replica, environment.id, sessionId); if (!session) { return new Response("Session not found", { status: 404 }); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.realtime.v1.streams.$runId.$streamId.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.realtime.v1.streams.$runId.$streamId.ts index cec6c3c4e9..6c50b0f146 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.realtime.v1.streams.$runId.$streamId.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.realtime.v1.streams.$runId.$streamId.ts @@ -82,14 +82,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // `request.signal` is severed by Remix's Request.clone() + Node undici GC bug // (see apps/webapp/CLAUDE.md). Use the Express res.on('close')-backed signal so // the upstream stream fetch actually aborts when the user closes the tab. - return realtimeStream.streamResponse( - request, - run.friendlyId, - streamId, - getRequestAbortSignal(), - { - lastEventId, - timeoutInSeconds, - } - ); + return realtimeStream.streamResponse(request, run.friendlyId, streamId, getRequestAbortSignal(), { + lastEventId, + timeoutInSeconds, + }); } 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 d6d39ebbb9..450b9c6966 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 @@ -397,10 +397,7 @@ function RunBody({ className="size-5 min-h-5 min-w-5" /> {run.taskIdentifier} @@ -1315,10 +1312,10 @@ function SpanEntity({ span }: { span: Span }) { span.isCancelled ? "CANCELED" : span.isError - ? "FAILED" - : span.isPartial - ? "EXECUTING" - : "COMPLETED" + ? "FAILED" + : span.isPartial + ? "EXECUTING" + : "COMPLETED" } className="text-sm" /> @@ -1430,10 +1427,10 @@ function SpanEntity({ span }: { span: Span }) { span.isCancelled ? "CANCELED" : span.isError - ? "FAILED" - : span.isPartial - ? "EXECUTING" - : "COMPLETED" + ? "FAILED" + : span.isPartial + ? "EXECUTING" + : "COMPLETED" } className="text-sm" /> @@ -1508,8 +1505,8 @@ function SpanEntity({ span }: { span: Span }) { typeof span.properties === "string" ? span.properties : span.properties != null - ? JSON.stringify(span.properties, null, 2) - : undefined + ? JSON.stringify(span.properties, null, 2) + : undefined } startTime={span.startTime} duration={span.duration} diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route.tsx index 24e7a73374..ee5eda4c3a 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route.tsx @@ -95,9 +95,15 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { { run } ); - return realtimeStream.streamResponse(request, run.friendlyId, streamKey, getRequestAbortSignal(), { - lastEventId, - }); + return realtimeStream.streamResponse( + request, + run.friendlyId, + streamKey, + getRequestAbortSignal(), + { + lastEventId, + } + ); }; export function RealtimeStreamViewer({ @@ -338,8 +344,8 @@ export function RealtimeStreamViewer({ chunks.length === 0 ? "cursor-not-allowed opacity-50" : copied - ? "text-success hover:cursor-pointer" - : "text-text-dimmed hover:cursor-pointer hover:text-text-bright" + ? "text-success hover:cursor-pointer" + : "text-text-dimmed hover:cursor-pointer hover:text-text-bright" )} > {copied ? ( diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.live.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.live.ts index 616aa72887..44def23a85 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.live.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.live.ts @@ -23,8 +23,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { Object.fromEntries(url.searchParams) ); - const newRunsSince = - includeNewRuns && since !== undefined ? since : undefined; + const newRunsSince = includeNewRuns && since !== undefined ? since : undefined; if (runIds.length === 0 && newRunsSince === undefined) { return { runs: [] }; diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx index 1f7a2add41..157462a86d 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx @@ -241,8 +241,8 @@ export function UpsertScheduleForm({ {schedule?.friendlyId ? "Edit schedule" : defaultTaskIdentifier - ? `New schedule for ${defaultTaskIdentifier}` - : "New schedule"} + ? `New schedule for ${defaultTaskIdentifier}` + : "New schedule"}
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.schedules-addon.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.schedules-addon.ts index f765da3534..8f09c6e4e1 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.schedules-addon.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.schedules-addon.ts @@ -14,10 +14,7 @@ export const PurchaseSchema = z.discriminatedUnion("action", [ }), z.object({ action: z.literal("quota-increase"), - amount: z.coerce - .number() - .int("Must be a whole number") - .min(1, "Amount must be greater than 0"), + amount: z.coerce.number().int("Must be a whole number").min(1, "Amount must be greater than 0"), }), ]); @@ -44,10 +41,9 @@ export async function action({ request, params }: ActionFunctionArgs) { ); } if (purchaseBlockReason === "managed_billing") { - return json( - { ok: false, error: "Contact us to request more schedules." } as const, - { status: 403 } - ); + return json({ ok: false, error: "Contact us to request more schedules." } as const, { + status: 403, + }); } const formData = await request.formData(); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx index 08dcad356c..b87c1bfc4c 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx @@ -531,10 +531,10 @@ export function TierFree({ {subscription?.plan === undefined ? "Select plan" : subscription.plan.type === "free" - ? "Current plan" - : subscription.canceledAt !== undefined - ? "Current plan" - : "Select plan"} + ? "Current plan" + : subscription.canceledAt !== undefined + ? "Current plan" + : "Select plan"} )} @@ -657,10 +657,10 @@ export function TierHobby({ {subscription?.plan === undefined ? "Select plan" : subscription.plan.type === "free" || subscription.canceledAt !== undefined - ? `Upgrade to ${plan.title}` - : subscription.plan.code === plan.code - ? "Current plan" - : `Upgrade to ${plan.title}`} + ? `Upgrade to ${plan.title}` + : subscription.plan.code === plan.code + ? "Current plan" + : `Upgrade to ${plan.title}`} )} @@ -802,10 +802,10 @@ export function TierPro({ {subscription?.plan === undefined ? "Select plan" : subscription.plan.type === "free" || subscription.canceledAt !== undefined - ? `Upgrade to ${plan.title}` - : subscription.plan.code === plan.code - ? "Current plan" - : `Upgrade to ${plan.title}`} + ? `Upgrade to ${plan.title}` + : subscription.plan.code === plan.code + ? "Current plan" + : `Upgrade to ${plan.title}`} )}
diff --git a/apps/webapp/app/routes/resources.preferences.sidemenu.tsx b/apps/webapp/app/routes/resources.preferences.sidemenu.tsx index 13f442aa61..94db448b58 100644 --- a/apps/webapp/app/routes/resources.preferences.sidemenu.tsx +++ b/apps/webapp/app/routes/resources.preferences.sidemenu.tsx @@ -4,10 +4,7 @@ import { SideMenuSectionIdSchema, type SideMenuSectionId, } from "~/components/navigation/sideMenuTypes"; -import { - updateItemOrder, - updateSideMenuPreferences, -} from "~/services/dashboardPreferences.server"; +import { updateItemOrder, updateSideMenuPreferences } from "~/services/dashboardPreferences.server"; import { requireUser } from "~/services/session.server"; // Transforms form data string "true"/"false" to boolean, or undefined if not present diff --git a/apps/webapp/app/routes/resources.runs.$runParam.logs.download.ts b/apps/webapp/app/routes/resources.runs.$runParam.logs.download.ts index 7662a88b4d..6b51403429 100644 --- a/apps/webapp/app/routes/resources.runs.$runParam.logs.download.ts +++ b/apps/webapp/app/routes/resources.runs.$runParam.logs.download.ts @@ -83,10 +83,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { return new Response("Not found", { status: 404 }); } - const eventRepository = await getEventRepositoryForStore( - run.taskEventStore, - run.organizationId - ); + const eventRepository = await getEventRepositoryForStore(run.taskEventStore, run.organizationId); // Stream the trace straight from the store to the gzip response, one event at // a time, never materialising the full set or building a tree. This keeps the diff --git a/apps/webapp/app/routes/resources.runs.$runParam.ts b/apps/webapp/app/routes/resources.runs.$runParam.ts index 38e17531f6..e85da45bbd 100644 --- a/apps/webapp/app/routes/resources.runs.$runParam.ts +++ b/apps/webapp/app/routes/resources.runs.$runParam.ts @@ -144,17 +144,17 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { finishedAttempt === null ? undefined : finishedAttempt.outputType === "application/store" - ? `/resources/packets/${run.runtimeEnvironment.id}/${finishedAttempt.output}` - : typeof finishedAttempt.output !== "undefined" && finishedAttempt.output !== null - ? await prettyPrintPacket(finishedAttempt.output, finishedAttempt.outputType ?? undefined) - : undefined; + ? `/resources/packets/${run.runtimeEnvironment.id}/${finishedAttempt.output}` + : typeof finishedAttempt.output !== "undefined" && finishedAttempt.output !== null + ? await prettyPrintPacket(finishedAttempt.output, finishedAttempt.outputType ?? undefined) + : undefined; const payload = run.payloadType === "application/store" ? `/resources/packets/${run.runtimeEnvironment.id}/${run.payload}` : typeof run.payload !== "undefined" && run.payload !== null - ? await prettyPrintPacket(run.payload, run.payloadType ?? undefined) - : undefined; + ? await prettyPrintPacket(run.payload, run.payloadType ?? undefined) + : undefined; let error: TaskRunError | undefined = undefined; if (finishedAttempt?.error) { diff --git a/apps/webapp/app/routes/resources.sessions.$sessionParam.close.ts b/apps/webapp/app/routes/resources.sessions.$sessionParam.close.ts index 27ffec5671..afc26a06e3 100644 --- a/apps/webapp/app/routes/resources.sessions.$sessionParam.close.ts +++ b/apps/webapp/app/routes/resources.sessions.$sessionParam.close.ts @@ -49,11 +49,7 @@ export const action: ActionFunction = async ({ request, params }) => { return json(submission); } - const session = await resolveSessionByIdOrExternalId( - $replica, - environment.id, - sessionParam - ); + const session = await resolveSessionByIdOrExternalId($replica, environment.id, sessionParam); if (!session) { submission.error = { sessionParam: ["Session not found"] }; diff --git a/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts b/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts index 49c2dbcf78..a068f6fa7e 100644 --- a/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts +++ b/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts @@ -224,7 +224,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ? undefined : regionForDisplay(run.region, run.workerQueue), regions: regionsResult.regions, - ttlSeconds: run.ttl ? parseDuration(run.ttl, "s") ?? undefined : undefined, + ttlSeconds: run.ttl ? (parseDuration(run.ttl, "s") ?? undefined) : undefined, idempotencyKey: run.idempotencyKey, runTags: run.runTags, payload, diff --git a/apps/webapp/app/routes/storybook.agent-ui/route.tsx b/apps/webapp/app/routes/storybook.agent-ui/route.tsx index 0bd205fa60..e43015a361 100644 --- a/apps/webapp/app/routes/storybook.agent-ui/route.tsx +++ b/apps/webapp/app/routes/storybook.agent-ui/route.tsx @@ -34,7 +34,8 @@ const fullDiagnosis: DiagnosisBlock = { reference: "error_emptyorder", }, ], - impact: "14 runs of process-order failed with this error in the last 24 hours, all in production.", + impact: + "14 runs of process-order failed with this error in the last 24 hours, all in production.", nextSteps: [ "Guard against an empty items array at the top of processOrder and return early.", "Validate the payload before triggering so empty orders never reach the task.", @@ -54,7 +55,11 @@ const externalServiceDiagnosis: DiagnosisBlock = { "The Stripe call has no timeout or retry, so a slow upstream response runs past the task's max duration.", confidence: "medium", evidence: [ - { type: "error", detail: "TimeoutError: Stripe API timed out after 30s", reference: "run_f6g7h8i9j0" }, + { + type: "error", + detail: "TimeoutError: Stripe API timed out after 30s", + reference: "run_f6g7h8i9j0", + }, { type: "deploy", detail: "First seen on version 20260620.2", reference: "20260620.2" }, ], impact: "Intermittent: 3 of the last 50 charge-payment runs timed out.", @@ -68,7 +73,8 @@ const externalServiceDiagnosis: DiagnosisBlock = { const lowConfidenceDiagnosis: DiagnosisBlock = { type: "diagnosis", runId: "run_k1l2m3n4o5", - summary: "The run crashed without a captured error, so the cause isn't conclusive from the available signals.", + summary: + "The run crashed without a captured error, so the cause isn't conclusive from the available signals.", category: "unknown", likelyCause: "The container exited without writing an error. This is consistent with an out-of-memory kill, but there's no OOM signal in the trace to confirm it.", diff --git a/apps/webapp/app/routes/storybook.animated-panel/route.tsx b/apps/webapp/app/routes/storybook.animated-panel/route.tsx index 2f136bddb1..f30be72b62 100644 --- a/apps/webapp/app/routes/storybook.animated-panel/route.tsx +++ b/apps/webapp/app/routes/storybook.animated-panel/route.tsx @@ -30,16 +30,76 @@ type DemoItem = { }; const demoItems: DemoItem[] = [ - { id: "run_a1b2c3d4", name: "Process invoices", status: "completed", duration: "2.3s", task: "invoice/process" }, - { id: "run_e5f6g7h8", name: "Send welcome email", status: "running", duration: "0.8s", task: "email/welcome" }, - { id: "run_i9j0k1l2", name: "Generate report", status: "failed", duration: "12.1s", task: "report/generate" }, - { id: "run_m3n4o5p6", name: "Sync inventory", status: "completed", duration: "5.7s", task: "inventory/sync" }, - { id: "run_q7r8s9t0", name: "Resize images", status: "queued", duration: "β€”", task: "image/resize" }, - { id: "run_u1v2w3x4", name: "Update search index", status: "completed", duration: "1.1s", task: "search/index" }, - { id: "run_y5z6a7b8", name: "Calculate analytics", status: "running", duration: "8.4s", task: "analytics/calc" }, - { id: "run_c9d0e1f2", name: "Deploy preview", status: "completed", duration: "34.2s", task: "deploy/preview" }, - { id: "run_g3h4i5j6", name: "Run migrations", status: "failed", duration: "0.3s", task: "db/migrate" }, - { id: "run_k7l8m9n0", name: "Notify Slack", status: "completed", duration: "0.5s", task: "notify/slack" }, + { + id: "run_a1b2c3d4", + name: "Process invoices", + status: "completed", + duration: "2.3s", + task: "invoice/process", + }, + { + id: "run_e5f6g7h8", + name: "Send welcome email", + status: "running", + duration: "0.8s", + task: "email/welcome", + }, + { + id: "run_i9j0k1l2", + name: "Generate report", + status: "failed", + duration: "12.1s", + task: "report/generate", + }, + { + id: "run_m3n4o5p6", + name: "Sync inventory", + status: "completed", + duration: "5.7s", + task: "inventory/sync", + }, + { + id: "run_q7r8s9t0", + name: "Resize images", + status: "queued", + duration: "β€”", + task: "image/resize", + }, + { + id: "run_u1v2w3x4", + name: "Update search index", + status: "completed", + duration: "1.1s", + task: "search/index", + }, + { + id: "run_y5z6a7b8", + name: "Calculate analytics", + status: "running", + duration: "8.4s", + task: "analytics/calc", + }, + { + id: "run_c9d0e1f2", + name: "Deploy preview", + status: "completed", + duration: "34.2s", + task: "deploy/preview", + }, + { + id: "run_g3h4i5j6", + name: "Run migrations", + status: "failed", + duration: "0.3s", + task: "db/migrate", + }, + { + id: "run_k7l8m9n0", + name: "Notify Slack", + status: "completed", + duration: "0.5s", + task: "notify/slack", + }, ]; const statusColors: Record = { @@ -144,10 +204,7 @@ export default function Story() {
- + - {/* Simple Line Chart (no zoom, but synced with date range) */} diff --git a/apps/webapp/app/routes/storybook.icons/route.tsx b/apps/webapp/app/routes/storybook.icons/route.tsx index 2b5f4c4cd3..dee5aa3e25 100644 --- a/apps/webapp/app/routes/storybook.icons/route.tsx +++ b/apps/webapp/app/routes/storybook.icons/route.tsx @@ -105,19 +105,12 @@ import { FlagEurope, FlagUSA } from "~/assets/icons/RegionIcons"; import { RightSideMenuIcon } from "~/assets/icons/RightSideMenuIcon"; import { RolesIcon } from "~/assets/icons/RolesIcon"; import { RunFunctionIcon } from "~/assets/icons/RunFunctionIcon"; -import { - RunsIcon, - RunsIconExtraSmall, - RunsIconSmall, -} from "~/assets/icons/RunsIcon"; +import { RunsIcon, RunsIconExtraSmall, RunsIconSmall } from "~/assets/icons/RunsIcon"; import { SaplingIcon } from "~/assets/icons/SaplingIcon"; import { ScheduleIcon } from "~/assets/icons/ScheduleIcon"; import { ShieldIcon } from "~/assets/icons/ShieldIcon"; import { ShieldLockIcon } from "~/assets/icons/ShieldLockIcon"; -import { - ShowParentIcon, - ShowParentIconSelected, -} from "~/assets/icons/ShowParentIcon"; +import { ShowParentIcon, ShowParentIconSelected } from "~/assets/icons/ShowParentIcon"; import { SideMenuRightClosedIcon } from "~/assets/icons/SideMenuRightClosed"; import { SlackIcon } from "~/assets/icons/SlackIcon"; import { SlackMonoIcon } from "~/assets/icons/SlackMonoIcon"; @@ -310,13 +303,8 @@ export default function Story() { key={icon.name} className="flex flex-col items-center gap-3 rounded-md border border-grid-bright bg-background-bright p-4 text-text-bright" > -
- {icon.render("size-6")} -
-
+
{icon.render("size-6")}
+
{icon.name}
diff --git a/apps/webapp/app/routes/storybook.timeline/route.tsx b/apps/webapp/app/routes/storybook.timeline/route.tsx index 24a1382fe5..e5606c1594 100644 --- a/apps/webapp/app/routes/storybook.timeline/route.tsx +++ b/apps/webapp/app/routes/storybook.timeline/route.tsx @@ -197,8 +197,8 @@ export default function Story() { index === 0 ? "left-0.5" : index === tickCount - 1 - ? "-right-0 -translate-x-full" - : "left-1/2 -translate-x-1/2" + ? "-right-0 -translate-x-full" + : "left-1/2 -translate-x-1/2" } > {formatDurationMilliseconds(ms, { diff --git a/apps/webapp/app/routes/vercel.callback.ts b/apps/webapp/app/routes/vercel.callback.ts index 6a188acfa3..cbd2ee9355 100644 --- a/apps/webapp/app/routes/vercel.callback.ts +++ b/apps/webapp/app/routes/vercel.callback.ts @@ -14,7 +14,7 @@ const VercelCallbackSchema = z error: z.string().optional(), error_description: z.string().optional(), configurationId: z.string().optional(), - next: z.string().optional() + next: z.string().optional(), }) .passthrough(); diff --git a/apps/webapp/app/routes/vercel.configure.tsx b/apps/webapp/app/routes/vercel.configure.tsx index 25b9197176..a9b4f4a151 100644 --- a/apps/webapp/app/routes/vercel.configure.tsx +++ b/apps/webapp/app/routes/vercel.configure.tsx @@ -16,7 +16,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { await requireUserId(request); const url = new URL(request.url); const searchParams = Object.fromEntries(url.searchParams); - + const { configurationId } = SearchParamsSchema.parse(searchParams); // Find the organization integration by configurationId (installationId in integrationData) @@ -49,4 +49,4 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { // This route doesn't render anything, it just redirects export default function VercelConfigurePage() { return null; -} \ No newline at end of file +} diff --git a/apps/webapp/app/routes/vercel.connect.tsx b/apps/webapp/app/routes/vercel.connect.tsx index faf43ca35e..6534e87d0e 100644 --- a/apps/webapp/app/routes/vercel.connect.tsx +++ b/apps/webapp/app/routes/vercel.connect.tsx @@ -23,7 +23,7 @@ async function createOrFindVercelIntegration( projectId: string, tokenResponse: TokenResponse, configurationId: string | undefined, - origin: 'marketplace' | 'dashboard' + origin: "marketplace" | "dashboard" ): Promise { const project = await prisma.project.findUnique({ where: { id: projectId }, @@ -47,7 +47,7 @@ async function createOrFindVercelIntegration( teamId: tokenResponse.teamId ?? null, userId: tokenResponse.userId, installationId: configurationId, - raw: tokenResponse.raw + raw: tokenResponse.raw, }); } else { await VercelIntegrationRepository.createVercelOrgIntegration({ @@ -146,7 +146,13 @@ export async function loader({ request }: LoaderFunctionArgs) { ); const result = await fromPromise( - createOrFindVercelIntegration(stateData.organizationId, stateData.projectId, tokenResponse, configurationId, origin), + createOrFindVercelIntegration( + stateData.organizationId, + stateData.projectId, + tokenResponse, + configurationId, + origin + ), (error) => error ); diff --git a/apps/webapp/app/routes/vercel.onboarding.tsx b/apps/webapp/app/routes/vercel.onboarding.tsx index 8187ff9282..94fd8f5347 100644 --- a/apps/webapp/app/routes/vercel.onboarding.tsx +++ b/apps/webapp/app/routes/vercel.onboarding.tsx @@ -151,11 +151,7 @@ export async function loader({ request }: LoaderFunctionArgs) { organizationId: params.data.organizationId, userId, }); - throw redirectWithErrorMessage( - "/", - request, - "Organization not found. Please try again." - ); + throw redirectWithErrorMessage("/", request, "Organization not found. Please try again."); } return typedjson({ @@ -216,11 +212,7 @@ export async function action({ request }: ActionFunctionArgs) { resumeParams.set("next", submission.data.next); } const resumeUrl = `/vercel/onboarding?${resumeParams.toString()}`; - const ssoRedirect = await ssoRedirectForEmail( - sessionUser.email, - "oauth_blocked", - resumeUrl - ); + const ssoRedirect = await ssoRedirectForEmail(sessionUser.email, "oauth_blocked", resumeUrl); if (ssoRedirect) { // The user is already authenticated via a non-SSO method, so a plain // redirect to `/login/sso` would be bounced straight home by that @@ -337,13 +329,12 @@ export default function VercelOnboardingPage() { return ( - + - @@ -369,7 +360,10 @@ export default function VercelOnboardingPage() { return ( - + } title="Select Organization" @@ -395,7 +389,8 @@ export default function VercelOnboardingPage() { defaultValue={data.organizations[0]?.id} text={(v) => typeof v === "string" - ? data.organizations.find((o) => o.id === v)?.title || "Choose an organization" + ? data.organizations.find((o) => o.id === v)?.title || + "Choose an organization" : "Choose an organization" } > @@ -445,7 +440,10 @@ export default function VercelOnboardingPage() { return ( - + } title="Select Project" @@ -472,7 +470,8 @@ export default function VercelOnboardingPage() { defaultValue={data.organization.projects[0]?.id} text={(v) => typeof v === "string" - ? data.organization.projects.find((p) => p.id === v)?.name || "Choose a project" + ? data.organization.projects.find((p) => p.id === v)?.name || + "Choose a project" : "Choose a project" } > diff --git a/apps/webapp/app/runEngine/concerns/batchGlobalRateLimiter.server.ts b/apps/webapp/app/runEngine/concerns/batchGlobalRateLimiter.server.ts index 9d808dd756..3b5da88087 100644 --- a/apps/webapp/app/runEngine/concerns/batchGlobalRateLimiter.server.ts +++ b/apps/webapp/app/runEngine/concerns/batchGlobalRateLimiter.server.ts @@ -33,4 +33,3 @@ export function createBatchGlobalRateLimiter(itemsPerSecond: number): GlobalRate }, }; } - diff --git a/apps/webapp/app/runEngine/concerns/computeMigration.server.ts b/apps/webapp/app/runEngine/concerns/computeMigration.server.ts index c885a80356..e598cbdca7 100644 --- a/apps/webapp/app/runEngine/concerns/computeMigration.server.ts +++ b/apps/webapp/app/runEngine/concerns/computeMigration.server.ts @@ -36,10 +36,10 @@ export function isOrgMigrated({ const pct = planType === "free" - ? flags?.computeMigrationFreePercentage ?? 0 + ? (flags?.computeMigrationFreePercentage ?? 0) : planType === "paid" - ? flags?.computeMigrationPaidPercentage ?? 0 - : 0; // enterprise / undefined + ? (flags?.computeMigrationPaidPercentage ?? 0) + : 0; // enterprise / undefined return hashBucket(orgId) < pct; } @@ -67,7 +67,11 @@ export function resolveComputeMigration({ backing, envType, ...decision -}: ResolveInput): { workerQueue: string | undefined; region: string | undefined; enableFastPath: boolean } { +}: ResolveInput): { + workerQueue: string | undefined; + region: string | undefined; + enableFastPath: boolean; +} { const passthrough = { workerQueue: baseWorkerQueue, region, enableFastPath: baseEnableFastPath }; if (baseWorkerQueue === undefined) return passthrough; if (envType === "DEVELOPMENT") return passthrough; diff --git a/apps/webapp/app/runEngine/concerns/idempotencyKeys.server.ts b/apps/webapp/app/runEngine/concerns/idempotencyKeys.server.ts index 02d0ec957f..3e049ceb37 100644 --- a/apps/webapp/app/runEngine/concerns/idempotencyKeys.server.ts +++ b/apps/webapp/app/runEngine/concerns/idempotencyKeys.server.ts @@ -66,7 +66,7 @@ export class IdempotencyKeyConcern { environmentId: string, organizationId: string, taskIdentifier: string, - idempotencyKey: string, + idempotencyKey: string ): Promise { const buffer = getMollifierBuffer(); if (!buffer) return null; @@ -111,10 +111,7 @@ export class IdempotencyKeyConcern { // accept goes through as a fresh trigger. Mirrors what // `ResetIdempotencyKeyService` does for the explicit // reset-via-API path. - if ( - synthetic.idempotencyKeyExpiresAt && - synthetic.idempotencyKeyExpiresAt < new Date() - ) { + if (synthetic.idempotencyKeyExpiresAt && synthetic.idempotencyKeyExpiresAt < new Date()) { const buffer = getMollifierBuffer(); if (buffer) { try { @@ -178,7 +175,7 @@ export class IdempotencyKeyConcern { request.environment.id, request.environment.organizationId, request.taskId, - idempotencyKey, + idempotencyKey ); if (buffered) { return { isCached: true, run: buffered }; @@ -307,18 +304,18 @@ export class IdempotencyKeyConcern { orgId: request.environment.organizationId, taskId: request.taskId, orgFeatureFlags: - ((request.environment.organization?.featureFlags as + (request.environment.organization?.featureFlags as | Record | null - | undefined) ?? null), + | undefined) ?? null, })); if (claimEligible) { const ttlSeconds = Math.max( 1, Math.min( env.TRIGGER_MOLLIFIER_CLAIM_TTL_SECONDS, - Math.ceil((idempotencyKeyExpiresAt.getTime() - Date.now()) / 1000), - ), + Math.ceil((idempotencyKeyExpiresAt.getTime() - Date.now()) / 1000) + ) ); const outcome = await claimOrAwait({ envId: request.environment.id, @@ -348,7 +345,7 @@ export class IdempotencyKeyConcern { request.environment.id, request.environment.organizationId, request.taskId, - idempotencyKey, + idempotencyKey ); if (buffered) { return { isCached: true, run: buffered }; @@ -381,10 +378,7 @@ export class IdempotencyKeyConcern { }); } if (outcome.kind === "timed_out") { - throw new ServiceValidationError( - "Idempotency claim resolution timed out", - 503, - ); + throw new ServiceValidationError("Idempotency claim resolution timed out", 503); } if (outcome.kind === "claimed") { // Caller MUST publish/release. Signalled via the result's diff --git a/apps/webapp/app/runEngine/concerns/payloads.server.ts b/apps/webapp/app/runEngine/concerns/payloads.server.ts index 7e1efbdf30..17e52109da 100644 --- a/apps/webapp/app/runEngine/concerns/payloads.server.ts +++ b/apps/webapp/app/runEngine/concerns/payloads.server.ts @@ -32,7 +32,13 @@ export class DefaultPayloadProcessor implements PayloadProcessor { const filename = `${request.friendlyId}/payload.json`; const [uploadError, uploadedFilename] = await tryCatch( - uploadPacketToObjectStore(filename, packet.data, packet.dataType, request.environment, env.OBJECT_STORE_DEFAULT_PROTOCOL) + uploadPacketToObjectStore( + filename, + packet.data, + packet.dataType, + request.environment, + env.OBJECT_STORE_DEFAULT_PROTOCOL + ) ); if (uploadError) { diff --git a/apps/webapp/app/runEngine/concerns/queues.server.ts b/apps/webapp/app/runEngine/concerns/queues.server.ts index 5fea4eedd8..5a4f18d1d6 100644 --- a/apps/webapp/app/runEngine/concerns/queues.server.ts +++ b/apps/webapp/app/runEngine/concerns/queues.server.ts @@ -16,7 +16,12 @@ import { env } from "~/env.server"; import { tryCatch } from "@trigger.dev/core/v3"; import { ServiceValidationError } from "~/v3/services/common.server"; import { isInfrastructureError } from "~/utils/prismaErrors"; -import { createCache, createLRUMemoryStore, DefaultStatefulContext, Namespace } from "@internal/cache"; +import { + createCache, + createLRUMemoryStore, + DefaultStatefulContext, + Namespace, +} from "@internal/cache"; import { singleton } from "~/utils/singleton"; import type { TaskMetadataCache, TaskMetadataEntry } from "~/services/taskMetadataCache.server"; import { taskMetadataCacheInstance } from "~/services/taskMetadataCacheInstance.server"; @@ -111,7 +116,8 @@ export class DefaultQueueManager implements QueueManager { if (!specifiedQueue) { throw new ServiceValidationError( - `Specified queue '${specifiedQueueName}' not found or not associated with locked version '${lockedBackgroundWorker.version ?? "" + `Specified queue '${specifiedQueueName}' not found or not associated with locked version '${ + lockedBackgroundWorker.version ?? "" }'.` ); } @@ -151,7 +157,8 @@ export class DefaultQueueManager implements QueueManager { if (!lockedMeta) { throw new ServiceValidationError( - `Task '${request.taskId}' not found on locked version '${lockedBackgroundWorker.version ?? "" + `Task '${request.taskId}' not found on locked version '${ + lockedBackgroundWorker.version ?? "" }'.` ); } @@ -167,7 +174,8 @@ export class DefaultQueueManager implements QueueManager { version: lockedBackgroundWorker.version, }); throw new ServiceValidationError( - `Default queue configuration for task '${request.taskId}' missing on locked version '${lockedBackgroundWorker.version ?? "" + `Default queue configuration for task '${request.taskId}' missing on locked version '${ + lockedBackgroundWorker.version ?? "" }'.` ); } @@ -474,7 +482,9 @@ export class DefaultQueueManager implements QueueManager { } } -export function getMaximumSizeForEnvironment(environment: AuthenticatedEnvironment): number | undefined { +export 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/batchTrigger.server.ts b/apps/webapp/app/runEngine/services/batchTrigger.server.ts index 3df2dfb00f..e450057799 100644 --- a/apps/webapp/app/runEngine/services/batchTrigger.server.ts +++ b/apps/webapp/app/runEngine/services/batchTrigger.server.ts @@ -16,7 +16,10 @@ import { env } from "~/env.server"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; import { batchTriggerWorker } from "~/v3/batchTriggerWorker.server"; -import { downloadPacketFromObjectStore, uploadPacketToObjectStore } from "../../v3/objectStore.server"; +import { + downloadPacketFromObjectStore, + uploadPacketToObjectStore, +} from "../../v3/objectStore.server"; import { ServiceValidationError, WithRunEngine } from "../../v3/services/baseService.server"; import { TriggerTaskService } from "../../v3/services/triggerTask.server"; import { startActiveSpan } from "../../v3/tracer.server"; @@ -559,11 +562,14 @@ export class RunEngineBatchTriggerService extends WithRunEngine { if (!runFriendlyId) { const errorMessage = "Trigger failed for batch item (queue limit, entitlement, or validation error)"; - logger.debug("[RunEngineBatchTrigger][processBatchTaskRun] Item trigger failed, creating pre-failed run", { - batchId: batch.friendlyId, - currentIndex: workingIndex, - task: item.task, - }); + logger.debug( + "[RunEngineBatchTrigger][processBatchTaskRun] Item trigger failed, creating pre-failed run", + { + batchId: batch.friendlyId, + currentIndex: workingIndex, + task: item.task, + } + ); const failedRunId = await triggerFailedTaskService.call({ taskId: item.task, @@ -583,10 +589,13 @@ export class RunEngineBatchTriggerService extends WithRunEngine { if (failedRunId) { runFriendlyId = failedRunId; } else { - logger.error("[RunEngineBatchTrigger][processBatchTaskRun] Failed to create pre-failed run", { - batchId: batch.friendlyId, - currentIndex: workingIndex, - }); + logger.error( + "[RunEngineBatchTrigger][processBatchTaskRun] Failed to create pre-failed run", + { + batchId: batch.friendlyId, + currentIndex: workingIndex, + } + ); return { status: "ERROR", @@ -717,7 +726,12 @@ export class RunEngineBatchTriggerService extends WithRunEngine { const filename = `${pathPrefix}/payload.json`; - const uploadedFilename = await uploadPacketToObjectStore(filename, packet.data, packet.dataType, environment); + const uploadedFilename = await uploadPacketToObjectStore( + filename, + packet.data, + packet.dataType, + environment + ); return { data: uploadedFilename, diff --git a/apps/webapp/app/runEngine/services/streamBatchItems.server.ts b/apps/webapp/app/runEngine/services/streamBatchItems.server.ts index d0d13adfe6..fd229777c1 100644 --- a/apps/webapp/app/runEngine/services/streamBatchItems.server.ts +++ b/apps/webapp/app/runEngine/services/streamBatchItems.server.ts @@ -447,9 +447,7 @@ export class StreamBatchItemsService extends WithRunEngine { if (!parseResult.success) { const rawIndex = (rawItem as { index?: unknown } | null)?.index; const where = typeof rawIndex === "number" ? `index ${rawIndex}` : "unknown index"; - throw new ServiceValidationError( - `Invalid item at ${where}: ${parseResult.error.message}` - ); + throw new ServiceValidationError(`Invalid item at ${where}: ${parseResult.error.message}`); } const item = parseResult.data; diff --git a/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts b/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts index 031411844b..6e3b73220f 100644 --- a/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts +++ b/apps/webapp/app/runEngine/services/triggerFailedTask.server.ts @@ -230,8 +230,7 @@ export class TriggerFailedTaskService { logger.warn("TriggerFailedTaskService: alert enqueue failed", { taskId: request.taskId, friendlyId: failedRun.friendlyId, - error: - alertsError instanceof Error ? alertsError.message : String(alertsError), + error: alertsError instanceof Error ? alertsError.message : String(alertsError), }); } @@ -337,8 +336,7 @@ export class TriggerFailedTaskService { logger.warn("TriggerFailedTaskService.callWithoutTraceEvents: alert enqueue failed", { taskId: opts.taskId, friendlyId: failedRun.friendlyId, - error: - alertsError instanceof Error ? alertsError.message : String(alertsError), + error: alertsError instanceof Error ? alertsError.message : String(alertsError), }); } diff --git a/apps/webapp/app/runEngine/services/triggerTask.server.ts b/apps/webapp/app/runEngine/services/triggerTask.server.ts index 89a938da8b..cee833e96e 100644 --- a/apps/webapp/app/runEngine/services/triggerTask.server.ts +++ b/apps/webapp/app/runEngine/services/triggerTask.server.ts @@ -30,10 +30,7 @@ import type { TriggerTaskServiceResult, } from "../../v3/services/triggerTask.server"; import { clampMaxDuration } from "../../v3/utils/maxDuration"; -import { - IdempotencyKeyConcern, - type ClaimedIdempotency, -} from "../concerns/idempotencyKeys.server"; +import { IdempotencyKeyConcern, type ClaimedIdempotency } from "../concerns/idempotencyKeys.server"; import { resolveScheduledQueueSplitEnabled, workerQueueForRun, @@ -235,7 +232,7 @@ export class RunEngineTriggerTaskService { if (debounceDelayError || !debounceDelayUntil) { throw new ServiceValidationError( `Invalid debounce delay: ${body.options.debounce.delay}. ` + - `Supported formats: {number}s, {number}m, {number}h, {number}d, {number}w` + `Supported formats: {number}s, {number}m, {number}h, {number}d, {number}w` ); } } @@ -243,12 +240,12 @@ export class RunEngineTriggerTaskService { // Get parent run if specified const parentRun = body.options?.parentRunId ? await runStore.findRun( - { - id: RunId.fromFriendlyId(body.options.parentRunId), - runtimeEnvironmentId: environment.id, - }, - this.prisma - ) + { + id: RunId.fromFriendlyId(body.options.parentRunId), + runtimeEnvironmentId: environment.id, + }, + this.prisma + ) : undefined; // Validate parent run @@ -271,8 +268,11 @@ export class RunEngineTriggerTaskService { return idempotencyKeyConcernResult; } - const { idempotencyKey, idempotencyKeyExpiresAt, claim: claimResult } = - idempotencyKeyConcernResult; + const { + idempotencyKey, + idempotencyKeyExpiresAt, + claim: claimResult, + } = idempotencyKeyConcernResult; // If we own an idempotency claim, the trigger pipeline below MUST // resolve it β€” publish on success so waiters see our runId, @@ -290,18 +290,18 @@ export class RunEngineTriggerTaskService { const lockedToBackgroundWorker = body.options?.lockToVersion ? await this.prisma.backgroundWorker.findFirst({ - where: { - projectId: environment.projectId, - runtimeEnvironmentId: environment.id, - version: body.options?.lockToVersion, - }, - select: { - id: true, - version: true, - sdkVersion: true, - cliVersion: true, - }, - }) + where: { + projectId: environment.projectId, + runtimeEnvironmentId: environment.id, + version: body.options?.lockToVersion, + }, + select: { + id: true, + version: true, + sdkVersion: true, + cliVersion: true, + }, + }) : undefined; const { queueName, lockedQueueId, taskTtl, taskKind } = @@ -340,10 +340,10 @@ export class RunEngineTriggerTaskService { const metadataPacket = body.options?.metadata ? handleMetadataPacket( - body.options?.metadata, - body.options?.metadataType ?? "application/json", - this.metadataMaximumSize - ) + body.options?.metadata, + body.options?.metadataType ?? "application/json", + this.metadataMaximumSize + ) : undefined; const tags = ( @@ -367,8 +367,12 @@ export class RunEngineTriggerTaskService { // from the in-memory snapshots (no DB query). A cold read (registry not yet // loaded) returns undefined/[] and the resolver falls back to not-migrated. const workerGroups = workerRegionRegistry.current() ?? []; - const region = baseWorkerQueue ? regionForQueue(baseWorkerQueue, workerGroups) : undefined; - const backing = baseWorkerQueue ? backingForQueue(baseWorkerQueue, workerGroups) : undefined; + const region = baseWorkerQueue + ? regionForQueue(baseWorkerQueue, workerGroups) + : undefined; + const backing = baseWorkerQueue + ? backingForQueue(baseWorkerQueue, workerGroups) + : undefined; const migrated = resolveComputeMigration({ baseWorkerQueue, baseEnableFastPath: enableFastPath, @@ -376,7 +380,10 @@ export class RunEngineTriggerTaskService { backing, planType, orgId: environment.organization.id, - orgFeatureFlags: environment.organization.featureFlags as Record | null, + orgFeatureFlags: environment.organization.featureFlags as Record< + string, + unknown + > | null, flags: globalFlagsRegistry.current(), envType: environment.type, }); @@ -470,8 +477,10 @@ export class RunEngineTriggerTaskService { orgId: environment.organizationId, taskId, orgFeatureFlags: - (environment.organization.featureFlags as Record | null) ?? - null, + (environment.organization.featureFlags as Record< + string, + unknown + > | null) ?? null, options: { debounce: body.options?.debounce, oneTimeUseToken: options.oneTimeUseToken, @@ -629,26 +638,26 @@ export class RunEngineTriggerTaskService { onDebounced: body.options?.debounce && body.options?.resumeParentOnCompletion ? async ({ existingRun, waitpoint, debounceKey }) => { - return await this.traceEventConcern.traceDebouncedRun( - triggerRequest, - parentRun?.taskEventStore, - { - existingRun, - debounceKey, - incomplete: waitpoint.status === "PENDING", - isError: waitpoint.outputIsError, - }, - async (spanEvent) => { - const spanId = - options?.parentAsLinkType === "replay" - ? spanEvent.spanId - : spanEvent.traceparent?.spanId - ? `${spanEvent.traceparent.spanId}:${spanEvent.spanId}` - : spanEvent.spanId; - return spanId; - } - ); - } + return await this.traceEventConcern.traceDebouncedRun( + triggerRequest, + parentRun?.taskEventStore, + { + existingRun, + debounceKey, + incomplete: waitpoint.status === "PENDING", + isError: waitpoint.outputIsError, + }, + async (spanEvent) => { + const spanId = + options?.parentAsLinkType === "replay" + ? spanEvent.spanId + : spanEvent.traceparent?.spanId + ? `${spanEvent.traceparent.spanId}:${spanEvent.spanId}` + : spanEvent.spanId; + return spanId; + } + ); + } : undefined, }, this.prisma @@ -703,7 +712,7 @@ export class RunEngineTriggerTaskService { throw error; } - }, + } ); // Pipeline returned successfully β€” publish the claim if we held // one. Waiters polling for our key resolve to this runId. @@ -745,13 +754,23 @@ export class RunEngineTriggerTaskService { workerQueue?: string; region?: string; enableFastPath: boolean; - lockedToBackgroundWorker?: { id: string; version: string; sdkVersion: string; cliVersion: string }; + lockedToBackgroundWorker?: { + id: string; + version: string; + sdkVersion: string; + cliVersion: string; + }; delayUntil?: Date; ttl?: string; metadataPacket?: { data?: string; dataType: string }; tags: string[]; depth: number; - parentRun?: { id: string; rootTaskRunId?: string | null; queueTimestamp?: Date | null; taskEventStore?: string }; + parentRun?: { + id: string; + rootTaskRunId?: string | null; + queueTimestamp?: Date | null; + taskEventStore?: string; + }; annotations: { triggerSource: string; triggerAction: string; @@ -826,7 +845,7 @@ export class RunEngineTriggerTaskService { queueTimestamp: args.options.queueTimestamp ?? (args.parentRun && args.body.options?.resumeParentOnCompletion - ? args.parentRun.queueTimestamp ?? undefined + ? (args.parentRun.queueTimestamp ?? undefined) : undefined), scheduleId: args.options.scheduleId, scheduleInstanceId: args.options.scheduleInstanceId, diff --git a/apps/webapp/app/runEngine/types.ts b/apps/webapp/app/runEngine/types.ts index c0c5de1d2f..339f1a36e1 100644 --- a/apps/webapp/app/runEngine/types.ts +++ b/apps/webapp/app/runEngine/types.ts @@ -37,13 +37,13 @@ export type TriggerTaskResult = { export type QueueValidationResult = | { - ok: true; - } + ok: true; + } | { - ok: false; - maximumSize: number; - queueSize: number; - }; + ok: false; + maximumSize: number; + queueSize: number; + }; export type QueueProperties = { queueName: string; @@ -99,22 +99,22 @@ export interface ParentRunValidationParams { export type ValidationResult = | { - ok: true; - } + ok: true; + } | { - ok: false; - error: Error; - }; + ok: false; + error: Error; + }; export type EntitlementValidationResult = | { - ok: true; - plan?: ReportUsagePlan; - } + ok: true; + plan?: ReportUsagePlan; + } | { - ok: false; - error: Error; - }; + ok: false; + error: Error; + }; export interface TriggerTaskValidator { validateTags(params: TagValidationParams): ValidationResult; diff --git a/apps/webapp/app/services/admin/missingLlmModels.server.ts b/apps/webapp/app/services/admin/missingLlmModels.server.ts index 07e6160ee0..2ad5bffb52 100644 --- a/apps/webapp/app/services/admin/missingLlmModels.server.ts +++ b/apps/webapp/app/services/admin/missingLlmModels.server.ts @@ -7,9 +7,11 @@ export type MissingLlmModel = { count: number; }; -export async function getMissingLlmModels(opts: { - lookbackHours?: number; -} = {}): Promise { +export async function getMissingLlmModels( + opts: { + lookbackHours?: number; + } = {} +): Promise { const lookbackHours = opts.lookbackHours ?? 24; const since = new Date(Date.now() - lookbackHours * 60 * 60 * 1000); @@ -100,14 +102,7 @@ export async function getMissingModelSamples(opts: { const createBuilder = adminClickhouse.reader.queryBuilderFast({ name: "missingModelSamples", table: "trigger_dev.task_events_v2", - columns: [ - "span_id", - "run_id", - "message", - "attributes_text", - "duration", - "start_time", - ], + columns: ["span_id", "run_id", "message", "attributes_text", "duration", "start_time"], }); const qb = createBuilder(); diff --git a/apps/webapp/app/services/apiAuth.server.ts b/apps/webapp/app/services/apiAuth.server.ts index 8297f95aa0..8e9a37f552 100644 --- a/apps/webapp/app/services/apiAuth.server.ts +++ b/apps/webapp/app/services/apiAuth.server.ts @@ -318,10 +318,10 @@ function getApiKeyResult(apiKey: string): { const type = isPublicApiKey(apiKey) ? "PUBLIC" : isSecretApiKey(apiKey) - ? "PRIVATE" - : isPublicJWT(apiKey) - ? "PUBLIC_JWT" - : "PRIVATE"; // Fallback to private key + ? "PRIVATE" + : isPublicJWT(apiKey) + ? "PUBLIC_JWT" + : "PRIVATE"; // Fallback to private key return { apiKey, type }; } @@ -351,7 +351,7 @@ const defaultAllowedAuthenticationMethods: AllowedAuthenticationMethods = { }; type FilteredAuthenticationResult< - T extends AllowedAuthenticationMethods = AllowedAuthenticationMethods + T extends AllowedAuthenticationMethods = AllowedAuthenticationMethods, > = | (T["personalAccessToken"] extends true ? Extract @@ -387,7 +387,7 @@ type FilteredAuthenticationResult< * ``` */ export async function authenticateRequest< - T extends AllowedAuthenticationMethods = AllowedAuthenticationMethods + T extends AllowedAuthenticationMethods = AllowedAuthenticationMethods, >( request: Request, allowedAuthenticationMethods?: T @@ -484,7 +484,10 @@ export async function authenticatedEnvironmentForAuthentication( ); } - if (auth.result.environment.slug !== slug && auth.result.environment.branchName !== resolvedBranch) { + if ( + auth.result.environment.slug !== slug && + auth.result.environment.branchName !== resolvedBranch + ) { throw json( { error: @@ -520,10 +523,10 @@ export async function authenticatedEnvironmentForAuthentication( slug: slug, ...(slug === "dev" ? { - orgMember: { - userId: user.id, - }, - } + orgMember: { + userId: user.id, + }, + } : {}), }, include: authIncludeBase, @@ -543,10 +546,10 @@ export async function authenticatedEnvironmentForAuthentication( branchName: resolvedBranch, ...(slug === "dev" ? { - orgMember: { - userId: user.id, - }, - } + orgMember: { + userId: user.id, + }, + } : {}), archivedAt: null, }, diff --git a/apps/webapp/app/services/attio.server.ts b/apps/webapp/app/services/attio.server.ts index 727a5b9ab2..c26eca8e37 100644 --- a/apps/webapp/app/services/attio.server.ts +++ b/apps/webapp/app/services/attio.server.ts @@ -33,7 +33,11 @@ class AttioClient { constructor(private readonly apiKey: string) {} // Create-or-update by unique attribute; returns the record id. Throws on failure so the worker retries. - async #assert(object: string, matchingAttribute: string, values: Record): Promise { + async #assert( + object: string, + matchingAttribute: string, + values: Record + ): Promise { const url = `${ATTIO_API}/objects/${object}/records?matching_attribute=${matchingAttribute}`; const response = await fetch(url, { method: "PUT", @@ -43,7 +47,12 @@ class AttioClient { if (!response.ok) { const body = await response.text(); - logger.error("Attio assert failed", { object, matchingAttribute, status: response.status, body }); + logger.error("Attio assert failed", { + object, + matchingAttribute, + status: response.status, + body, + }); throw new Error(`Attio assert ${object} failed with status ${response.status}`); } @@ -105,7 +114,11 @@ export async function enqueueAttioWorkspaceSync(payload: AttioWorkspaceSync) { try { // Lazy import to avoid a circular dependency with commonWorker (which imports this module's schemas). const { commonWorker } = await import("~/v3/commonWorker.server"); - await commonWorker.enqueue({ id: `attio:workspace:${payload.orgId}`, job: "attio.syncWorkspace", payload }); + await commonWorker.enqueue({ + id: `attio:workspace:${payload.orgId}`, + job: "attio.syncWorkspace", + payload, + }); } catch (error) { logger.error("Failed to enqueue Attio workspace sync", { orgId: payload.orgId, error }); } @@ -115,7 +128,11 @@ export async function enqueueAttioUserSync(payload: AttioUserSync) { if (!attioClient) return; try { const { commonWorker } = await import("~/v3/commonWorker.server"); - await commonWorker.enqueue({ id: `attio:user:${payload.userId}`, job: "attio.syncUser", payload }); + await commonWorker.enqueue({ + id: `attio:user:${payload.userId}`, + job: "attio.syncUser", + payload, + }); } catch (error) { logger.error("Failed to enqueue Attio user sync", { userId: payload.userId, error }); } diff --git a/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts b/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts index 431a0062db..6eb3bb08ba 100644 --- a/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts +++ b/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts @@ -138,8 +138,8 @@ export function createLimiterFromConfig(config: RateLimiterConfig): Limiter { return config.type === "fixedWindow" ? Ratelimit.fixedWindow(config.tokens, config.window) : config.type === "tokenBucket" - ? Ratelimit.tokenBucket(config.refillRate, config.interval, config.maxTokens) - : Ratelimit.slidingWindow(config.tokens, config.window); + ? Ratelimit.tokenBucket(config.refillRate, config.interval, config.maxTokens) + : Ratelimit.slidingWindow(config.tokens, config.window); } //returns an Express middleware that rate limits using the Bearer token in the Authorization header diff --git a/apps/webapp/app/services/betterstack/betterstack.server.ts b/apps/webapp/app/services/betterstack/betterstack.server.ts index 95fe220883..0a097458e4 100644 --- a/apps/webapp/app/services/betterstack/betterstack.server.ts +++ b/apps/webapp/app/services/betterstack/betterstack.server.ts @@ -42,9 +42,7 @@ export type IncidentStatus = { title: string | null; }; -type CachedResult = - | { success: true; data: IncidentStatus } - | { success: false; error: unknown }; +type CachedResult = { success: true; data: IncidentStatus } | { success: false; error: unknown }; const ctx = new DefaultStatefulContext(); const memory = createLRUMemoryStore(100); @@ -79,10 +77,7 @@ export class BetterStackClient { return cachedResult.val; } - private async fetchIncidentStatus( - apiKey: string, - statusPageId: string - ): Promise { + private async fetchIncidentStatus(apiKey: string, statusPageId: string): Promise { const headers = { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", diff --git a/apps/webapp/app/services/dashboardAgent.server.ts b/apps/webapp/app/services/dashboardAgent.server.ts index 273b16edd3..8e8b8884e3 100644 --- a/apps/webapp/app/services/dashboardAgent.server.ts +++ b/apps/webapp/app/services/dashboardAgent.server.ts @@ -91,7 +91,10 @@ export type DashboardAgentRepoSnapshot = { // The GitHub archive redirect URL is valid for a few minutes; cache the resolved // pointer briefly so multi-turn chats don't re-mint a token + re-resolve on every // message. Keyed by project + ref. -const repoSnapshotCache = new Map(); +const repoSnapshotCache = new Map< + string, + { snapshot: DashboardAgentRepoSnapshot; expiresAt: number } +>(); const REPO_SNAPSHOT_TTL_MS = 60_000; const REPO_SNAPSHOT_MAX_ENTRIES = 1_000; diff --git a/apps/webapp/app/services/dashboardAgentHeadStart.server.ts b/apps/webapp/app/services/dashboardAgentHeadStart.server.ts index ff9039f3ef..d30f8da7ab 100644 --- a/apps/webapp/app/services/dashboardAgentHeadStart.server.ts +++ b/apps/webapp/app/services/dashboardAgentHeadStart.server.ts @@ -34,8 +34,7 @@ export async function startDashboardAgentHeadStart(params: { mode: "assistant" | "code"; metadata: Record; }): Promise { - const tools = - params.mode === "code" ? dashboardAgentCodeToolSchemas : dashboardAgentToolSchemas; + const tools = params.mode === "code" ? dashboardAgentCodeToolSchemas : dashboardAgentToolSchemas; const system = params.mode === "code" ? DASHBOARD_AGENT_CODE_SYSTEM_PROMPT : DASHBOARD_AGENT_SYSTEM_PROMPT; diff --git a/apps/webapp/app/services/dataStores/organizationDataStoresRegistry.server.ts b/apps/webapp/app/services/dataStores/organizationDataStoresRegistry.server.ts index 07a4f1d359..bf495e885c 100644 --- a/apps/webapp/app/services/dataStores/organizationDataStoresRegistry.server.ts +++ b/apps/webapp/app/services/dataStores/organizationDataStoresRegistry.server.ts @@ -149,7 +149,6 @@ export class OrganizationDataStoresRegistry { config: { version: 1, data: { secretKey } }, }, }); - } async updateDataStore({ diff --git a/apps/webapp/app/services/environmentMetricsRepository.server.ts b/apps/webapp/app/services/environmentMetricsRepository.server.ts index e06ad05cbe..61b53a4d6c 100644 --- a/apps/webapp/app/services/environmentMetricsRepository.server.ts +++ b/apps/webapp/app/services/environmentMetricsRepository.server.ts @@ -152,7 +152,7 @@ type TaskActivityResults = Array<{ taskIdentifier: string; status: TaskRunStatus; day: Date; - count: BigInt; + count: bigint; }>; function fillInDailyTaskActivity(activity: TaskActivityResults, days: number): DailyTaskActivity { @@ -196,7 +196,7 @@ function fillInDailyTaskActivity(activity: TaskActivityResults, days: number): D type CurrentRunningStatsResults = Array<{ taskIdentifier: string; status: TaskRunStatus; - count: BigInt; + count: bigint; }>; function fillInCurrentRunningStats( diff --git a/apps/webapp/app/services/environmentVariableApiAccess.server.ts b/apps/webapp/app/services/environmentVariableApiAccess.server.ts index 49d745ddad..11d401125a 100644 --- a/apps/webapp/app/services/environmentVariableApiAccess.server.ts +++ b/apps/webapp/app/services/environmentVariableApiAccess.server.ts @@ -43,7 +43,10 @@ export async function authorizePatEnvironmentAccess({ resource: EnvironmentScopedResource; action: "read" | "write"; }): Promise { - const bearer = request.headers.get("Authorization")?.replace(/^Bearer /, "").trim(); + const bearer = request.headers + .get("Authorization") + ?.replace(/^Bearer /, "") + .trim(); const isUat = !!bearer && isUserActorToken(bearer); // Machine creds (apiKey) and org tokens carry no user role to enforce. A diff --git a/apps/webapp/app/services/gitHub.server.ts b/apps/webapp/app/services/gitHub.server.ts index 4363c68050..e67895fe06 100644 --- a/apps/webapp/app/services/gitHub.server.ts +++ b/apps/webapp/app/services/gitHub.server.ts @@ -45,8 +45,8 @@ export async function linkGitHubAppInstallation( ? "login" in installation.account ? installation.account.login : "slug" in installation.account - ? installation.account.slug - : "-" + ? installation.account.slug + : "-" : "-", permissions: installation.permissions, repositorySelection, @@ -91,8 +91,8 @@ export async function updateGitHubAppInstallation(installationId: number): Promi ? "login" in installation.account ? installation.account.login : "slug" in installation.account - ? installation.account.slug - : "-" + ? installation.account.slug + : "-" : "-", permissions: installation.permissions, suspendedAt: existingInstallation?.suspendedAt, diff --git a/apps/webapp/app/services/impersonation.server.ts b/apps/webapp/app/services/impersonation.server.ts index 288fd635ae..a4f3ee59c9 100644 --- a/apps/webapp/app/services/impersonation.server.ts +++ b/apps/webapp/app/services/impersonation.server.ts @@ -56,18 +56,16 @@ function getImpersonationTokenSecret(): Uint8Array { } function getImpersonationTokenRedisClient(): RedisClient { - return singleton( - "impersonationTokenRedis", - () => - createRedisClient("impersonation:token", { - host: env.CACHE_REDIS_HOST, - port: env.CACHE_REDIS_PORT, - username: env.CACHE_REDIS_USERNAME, - password: env.CACHE_REDIS_PASSWORD, - tlsDisabled: env.CACHE_REDIS_TLS_DISABLED === "true", - clusterMode: env.CACHE_REDIS_CLUSTER_MODE_ENABLED === "1", - keyPrefix: "impersonation:token:", - }) + return singleton("impersonationTokenRedis", () => + createRedisClient("impersonation:token", { + host: env.CACHE_REDIS_HOST, + port: env.CACHE_REDIS_PORT, + username: env.CACHE_REDIS_USERNAME, + password: env.CACHE_REDIS_PASSWORD, + tlsDisabled: env.CACHE_REDIS_TLS_DISABLED === "true", + clusterMode: env.CACHE_REDIS_CLUSTER_MODE_ENABLED === "1", + keyPrefix: "impersonation:token:", + }) ); } @@ -134,9 +132,12 @@ export async function validateAndConsumeImpersonationToken( return undefined; } } catch (redisError) { - logger.warn("Redis unavailable for impersonation token tracking, relying on JWT expiry only", { - error: redisError instanceof Error ? redisError.message : String(redisError), - }); + logger.warn( + "Redis unavailable for impersonation token tracking, relying on JWT expiry only", + { + error: redisError instanceof Error ? redisError.message : String(redisError), + } + ); } } diff --git a/apps/webapp/app/services/loadProjectEnvironmentFromRequest.server.ts b/apps/webapp/app/services/loadProjectEnvironmentFromRequest.server.ts index 76c39caba6..f317a4c58c 100644 --- a/apps/webapp/app/services/loadProjectEnvironmentFromRequest.server.ts +++ b/apps/webapp/app/services/loadProjectEnvironmentFromRequest.server.ts @@ -4,10 +4,7 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { requireUserId } from "~/services/session.server"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; -export async function loadProjectEnvironmentFromRequest( - request: Request, - params: Params -) { +export async function loadProjectEnvironmentFromRequest(request: Request, params: Params) { const userId = await requireUserId(request); const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); diff --git a/apps/webapp/app/services/org.server.ts b/apps/webapp/app/services/org.server.ts index 75c1467ab2..ebf7e7a765 100644 --- a/apps/webapp/app/services/org.server.ts +++ b/apps/webapp/app/services/org.server.ts @@ -3,7 +3,7 @@ import { requireUserId } from "./session.server"; export async function requireOrganization(request: Request, organizationSlug: string) { const userId = await requireUserId(request); - + const organization = await prisma.organization.findFirst({ where: { slug: organizationSlug, diff --git a/apps/webapp/app/services/personalAccessToken.server.ts b/apps/webapp/app/services/personalAccessToken.server.ts index acd3959911..a0821d6602 100644 --- a/apps/webapp/app/services/personalAccessToken.server.ts +++ b/apps/webapp/app/services/personalAccessToken.server.ts @@ -131,10 +131,7 @@ export async function updateLastAccessedAtIfStale( tokenId: string, lastAccessedAt: Date | null ): Promise { - if ( - lastAccessedAt && - Date.now() - lastAccessedAt.getTime() <= PAT_LAST_ACCESSED_THROTTLE_MS - ) { + if (lastAccessedAt && Date.now() - lastAccessedAt.getTime() <= PAT_LAST_ACCESSED_THROTTLE_MS) { return; // fresh β€” no roundtrip } await prisma.personalAccessToken.updateMany({ diff --git a/apps/webapp/app/services/platform.v3.server.ts b/apps/webapp/app/services/platform.v3.server.ts index fdc709fb1b..9d10210154 100644 --- a/apps/webapp/app/services/platform.v3.server.ts +++ b/apps/webapp/app/services/platform.v3.server.ts @@ -62,8 +62,7 @@ const platformClientMeter = metrics.getMeter("trigger.dev/platform-client"); const platformClientFailuresCounter = platformClientMeter.createCounter( "platform_client.failures_total", { - description: - "Failures returned or thrown by @trigger.dev/platform billing client calls", + description: "Failures returned or thrown by @trigger.dev/platform billing client calls", } ); @@ -71,7 +70,6 @@ function recordPlatformFailure(fn: string, kind: "caught" | "no_success") { platformClientFailuresCounter.add(1, { function: fn, kind }); } - function initializePlatformCache() { const ctx = new DefaultStatefulContext(); const memory = createLRUMemoryStore(1000); diff --git a/apps/webapp/app/services/platformNotifications.server.ts b/apps/webapp/app/services/platformNotifications.server.ts index 6425b7bdb6..19faa03821 100644 --- a/apps/webapp/app/services/platformNotifications.server.ts +++ b/apps/webapp/app/services/platformNotifications.server.ts @@ -1,7 +1,10 @@ import { z } from "zod"; import { errAsync, fromPromise, type ResultAsync } from "neverthrow"; import { prisma } from "~/db.server"; -import { type PlatformNotificationScope, type PlatformNotificationSurface } from "@trigger.dev/database"; +import { + type PlatformNotificationScope, + type PlatformNotificationSurface, +} from "@trigger.dev/database"; import { incrementCliRequestCounter } from "./platformNotificationCounter.server"; // --- Payload schema (spec v1) --- @@ -65,9 +68,7 @@ export async function getAdminNotificationsList({ pageSize?: number; hideInactive?: boolean; }) { - const where = hideInactive - ? { archivedAt: null, endsAt: { gt: new Date() } } - : {}; + const where = hideInactive ? { archivedAt: null, endsAt: { gt: new Date() } } : {}; const [notifications, total] = await Promise.all([ prisma.platformNotification.findMany({ @@ -114,7 +115,9 @@ export async function getAdminNotificationsList({ payloadDescription: parsed.success ? parsed.data.data.description : null, payloadActionUrl: parsed.success ? parsed.data.data.actionUrl : null, payloadImage: parsed.success ? parsed.data.data.image : null, - payloadDismissOnAction: parsed.success ? (parsed.data.data.dismissOnAction ?? false) : false, + payloadDismissOnAction: parsed.success + ? (parsed.data.data.dismissOnAction ?? false) + : false, payloadDiscovery: parsed.success ? (parsed.data.data.discovery ?? null) : null, cliMaxShowCount: n.cliMaxShowCount, cliMaxDaysAfterFirstSeen: n.cliMaxDaysAfterFirstSeen, @@ -514,8 +517,7 @@ export async function getNextCliNotification({ // If this display reaches cliMaxShowCount, also set cliDismissedAt now // so it's recorded immediately rather than waiting for a future request. const reachedMaxShows = - n.cliMaxShowCount !== null && - ((interaction?.showCount ?? 0) + 1) >= n.cliMaxShowCount; + n.cliMaxShowCount !== null && (interaction?.showCount ?? 0) + 1 >= n.cliMaxShowCount; const updated = await prisma.platformNotificationInteraction.upsert({ where: { notificationId_userId: { notificationId: n.id, userId } }, @@ -699,9 +701,7 @@ export const UpdatePlatformNotificationSchema = z validateEndsAt(data, ctx); }); -type CreateError = - | { type: "validation"; issues: z.ZodIssue[] } - | { type: "db"; message: string }; +type CreateError = { type: "validation"; issues: z.ZodIssue[] } | { type: "db"; message: string }; export function createPlatformNotification( input: CreatePlatformNotificationInput @@ -765,7 +765,8 @@ export function updatePlatformNotification( startsAt: data.startsAt, endsAt: data.endsAt, priority: data.priority, - cliMaxDaysAfterFirstSeen: data.surface === "CLI" ? (data.cliMaxDaysAfterFirstSeen ?? null) : null, + cliMaxDaysAfterFirstSeen: + data.surface === "CLI" ? (data.cliMaxDaysAfterFirstSeen ?? null) : null, cliMaxShowCount: data.surface === "CLI" ? (data.cliMaxShowCount ?? null) : null, cliShowEvery: data.surface === "CLI" ? (data.cliShowEvery ?? null) : null, }, diff --git a/apps/webapp/app/services/queryConcurrencyLimiter.server.ts b/apps/webapp/app/services/queryConcurrencyLimiter.server.ts index b623f018b1..a25b954510 100644 --- a/apps/webapp/app/services/queryConcurrencyLimiter.server.ts +++ b/apps/webapp/app/services/queryConcurrencyLimiter.server.ts @@ -26,4 +26,3 @@ export const DEFAULT_ORG_CONCURRENCY_LIMIT = env.QUERY_DEFAULT_ORG_CONCURRENCY_L /** Global concurrency limit from environment */ export const GLOBAL_CONCURRENCY_LIMIT = env.QUERY_GLOBAL_CONCURRENCY_LIMIT; - diff --git a/apps/webapp/app/services/queryService.server.ts b/apps/webapp/app/services/queryService.server.ts index 4a576c2bf3..57b877ed87 100644 --- a/apps/webapp/app/services/queryService.server.ts +++ b/apps/webapp/app/services/queryService.server.ts @@ -224,8 +224,7 @@ export async function executeQuery( scope === "project" || scope === "environment" ? { op: "eq", value: projectId } : undefined, - environment_id: - scope === "environment" ? { op: "eq", value: environmentId } : undefined, + environment_id: scope === "environment" ? { op: "eq", value: environmentId } : undefined, }), [timeColumn]: { op: "gte", value: maxQueryPeriodDate }, // Optional filters for tasks and queues @@ -246,8 +245,7 @@ export async function executeQuery( : undefined, operation_id: operations && operations.length > 0 ? { op: "in", values: operations } : undefined, - gen_ai_system: - providers && providers.length > 0 ? { op: "in", values: providers } : undefined, + gen_ai_system: providers && providers.length > 0 ? { op: "in", values: providers } : undefined, } satisfies Record; // Compute the effective time range for timeBucket() interval calculation @@ -275,7 +273,10 @@ export async function executeQuery( environment: Object.fromEntries(environments.map((e) => [e.id, e.slug])), }; - const queryClickhouse = await clickhouseFactory.getClickhouseForOrganization(organizationId, "query"); + const queryClickhouse = await clickhouseFactory.getClickhouseForOrganization( + organizationId, + "query" + ); const result = await executeTSQL(queryClickhouse.reader, { ...baseOptions, schema: z.record(z.any()), diff --git a/apps/webapp/app/services/realtime/duration.server.ts b/apps/webapp/app/services/realtime/duration.server.ts index c6aab9eb9d..b69c63ee74 100644 --- a/apps/webapp/app/services/realtime/duration.server.ts +++ b/apps/webapp/app/services/realtime/duration.server.ts @@ -28,20 +28,19 @@ export function parseDuration(input: string): number { } const value = parseInt(match[1]!, 10); const unit = match[2]!; - const multiplier = - /^s/.test(unit) - ? 1 - : /^m(?:in|ins|inute|inutes)?$/.test(unit) - ? 60 - : /^h/.test(unit) - ? 3600 - : /^d/.test(unit) - ? 86400 - : /^w/.test(unit) - ? 604800 - : /^y/.test(unit) - ? 31_536_000 - : NaN; + const multiplier = /^s/.test(unit) + ? 1 + : /^m(?:in|ins|inute|inutes)?$/.test(unit) + ? 60 + : /^h/.test(unit) + ? 3600 + : /^d/.test(unit) + ? 86400 + : /^w/.test(unit) + ? 604800 + : /^y/.test(unit) + ? 31_536_000 + : NaN; if (!Number.isFinite(multiplier)) { throw new Error(`Invalid duration unit: ${unit}`); } diff --git a/apps/webapp/app/services/realtime/electricStreamProtocol.server.ts b/apps/webapp/app/services/realtime/electricStreamProtocol.server.ts index efe711a727..fc3a3285af 100644 --- a/apps/webapp/app/services/realtime/electricStreamProtocol.server.ts +++ b/apps/webapp/app/services/realtime/electricStreamProtocol.server.ts @@ -185,7 +185,9 @@ export function buildElectricSchemaHeader(skipColumns: string[] = []): string { if (skip.has(column.name)) { continue; } - schema[column.name] = column.dims ? { type: column.type, dims: column.dims } : { type: column.type }; + schema[column.name] = column.dims + ? { type: column.type, dims: column.dims } + : { type: column.type }; } return JSON.stringify(schema); diff --git a/apps/webapp/app/services/realtime/envChangeRouter.server.ts b/apps/webapp/app/services/realtime/envChangeRouter.server.ts index 6c5969e02c..95e25b7d0c 100644 --- a/apps/webapp/app/services/realtime/envChangeRouter.server.ts +++ b/apps/webapp/app/services/realtime/envChangeRouter.server.ts @@ -588,9 +588,7 @@ export class EnvChangeRouter { staleRunIds.clear(); } const echoHorizonMs = gate.maxDelayMs * 10; - const newestWatermarkMs = Math.max( - ...staleRecords.map((record) => record.updatedAtMs ?? 0) - ); + const newestWatermarkMs = Math.max(...staleRecords.map((record) => record.updatedAtMs ?? 0)); const withinEchoHorizon = Date.now() - newestWatermarkMs < echoHorizonMs; if (attempt < gate.staleRetries || withinEchoHorizon) { const retryDelayMs = Math.max( @@ -685,7 +683,10 @@ export class EnvChangeRouter { /** Authoritative re-check for tag feeds: the hydrated row carries ALL the filter's tags * (Electric's `runTags @> ARRAY[...]` semantics) and its createdAt is within the window. */ #tagRowMatches(row: RealtimeRunRow, filter: Extract): boolean { - if (filter.createdAtFloorMs !== undefined && row.createdAt.getTime() < filter.createdAtFloorMs) { + if ( + filter.createdAtFloorMs !== undefined && + row.createdAt.getTime() < filter.createdAtFloorMs + ) { return false; } const rowTags = row.runTags ?? []; diff --git a/apps/webapp/app/services/realtime/nativeRealtimeClient.server.ts b/apps/webapp/app/services/realtime/nativeRealtimeClient.server.ts index 00e50ed9fc..8b49e52cfd 100644 --- a/apps/webapp/app/services/realtime/nativeRealtimeClient.server.ts +++ b/apps/webapp/app/services/realtime/nativeRealtimeClient.server.ts @@ -26,11 +26,7 @@ import { type SerializedRowChange, } from "./electricStreamProtocol.server"; import { BoundedTtlCache } from "./boundedTtlCache"; -import { - type EnvChangeRouter, - type FeedFilter, - type MatchedRow, -} from "./envChangeRouter.server"; +import { type EnvChangeRouter, type FeedFilter, type MatchedRow } from "./envChangeRouter.server"; import { type RunHydrator, type RunListResolver } from "./runReader.server"; import { type RealtimeConcurrencyLimiter } from "./realtimeConcurrencyLimiter.server"; import { InMemoryReplayCursorStore, type ReplayCursorStore } from "./replayCursorStore.server"; @@ -391,7 +387,9 @@ export class NativeRealtimeClient implements RealtimeStreamClient { existingHandle?: string ): Response { const body = buildSnapshotBody(row, skipColumns); - const offset = row ? encodeOffset(row.updatedAt.getTime(), this.#nextSeq()) : encodeOffset(0, 0); + const offset = row + ? encodeOffset(row.updatedAt.getTime(), this.#nextSeq()) + : encodeOffset(0, 0); return this.#buildResponse(body, apiVersion, clientVersion, { offset, handle: existingHandle ?? this.#mintHandle(runId), @@ -568,14 +566,22 @@ export class NativeRealtimeClient implements RealtimeStreamClient { let prevSeen = this.#workingSetCache.get(workingSetKey); const markPollEnd = () => this.#replayCursors.set(workingSetKey, Date.now()); - const emitFromSerialized = (changes: SerializedRowChange[], maxUpdatedAt: number): Response => { + const emitFromSerialized = ( + changes: SerializedRowChange[], + maxUpdatedAt: number + ): Response => { const seq = this.#nextSeq(); markPollEnd(); - return this.#buildResponse(buildRowsBodyFromSerialized(changes), apiVersion, clientVersion, { - offset: encodeOffset(maxUpdatedAt, seq), - handle, - cursor: String(seq), - }); + return this.#buildResponse( + buildRowsBodyFromSerialized(changes), + apiVersion, + clientVersion, + { + offset: encodeOffset(maxUpdatedAt, seq), + handle, + cursor: String(seq), + } + ); }; const emitFromRows = (changes: RowChange[], maxUpdatedAt: number): Response => { const seq = this.#nextSeq(); @@ -853,7 +859,8 @@ export class NativeRealtimeClient implements RealtimeStreamClient { * same projected columns, so cached rows always match the requesting feed. */ #runSetCacheKey(environmentId: string, filter: RunSetFilter, skipColumns: string[]): string { // JSON-encode the arrays (not a join) so a tag containing the separator can't collide with a different filter. - const tags = filter.tags && filter.tags.length > 0 ? JSON.stringify([...filter.tags].sort()) : ""; + const tags = + filter.tags && filter.tags.length > 0 ? JSON.stringify([...filter.tags].sort()) : ""; const cols = skipColumns.length > 0 ? JSON.stringify([...skipColumns].sort()) : ""; const maxListResults = this.options.maxListResults ?? DEFAULT_MAX_LIST_RESULTS; return `${environmentId}|${tags}|${filter.batchId ?? ""}|${ @@ -1040,7 +1047,8 @@ export class NativeRealtimeClient implements RealtimeStreamClient { } #resolveSkipColumns(url: URL, requestOptions?: RealtimeRequestOptions): string[] { - const raw = requestOptions?.skipColumns ?? url.searchParams.get("skipColumns")?.split(",") ?? []; + const raw = + requestOptions?.skipColumns ?? url.searchParams.get("skipColumns")?.split(",") ?? []; return raw.map((c) => c.trim()).filter((c) => c !== "" && !RESERVED_COLUMNS.includes(c)); } } diff --git a/apps/webapp/app/services/realtime/realtimeConcurrencyLimiter.server.ts b/apps/webapp/app/services/realtime/realtimeConcurrencyLimiter.server.ts index 1b6fdb3b0b..414ffec9f7 100644 --- a/apps/webapp/app/services/realtime/realtimeConcurrencyLimiter.server.ts +++ b/apps/webapp/app/services/realtime/realtimeConcurrencyLimiter.server.ts @@ -27,7 +27,11 @@ export class RealtimeConcurrencyLimiter { this.#registerCommands(); } - async incrementAndCheck(environmentId: string, requestId: string, limit: number): Promise { + async incrementAndCheck( + environmentId: string, + requestId: string, + limit: number + ): Promise { const key = this.#getKey(environmentId); const now = Date.now(); diff --git a/apps/webapp/app/services/realtime/resolveRealtimeStreamClient.server.ts b/apps/webapp/app/services/realtime/resolveRealtimeStreamClient.server.ts index 69ca81cf2c..4b2207fe19 100644 --- a/apps/webapp/app/services/realtime/resolveRealtimeStreamClient.server.ts +++ b/apps/webapp/app/services/realtime/resolveRealtimeStreamClient.server.ts @@ -37,7 +37,7 @@ export async function resolveRealtimeStreamClient( // The authenticated environment already carries the org's feature flags; pass them // through so a cache miss doesn't need an extra organization read. const orgFeatureFlags = environment.organization - ? environment.organization.featureFlags ?? {} + ? (environment.organization.featureFlags ?? {}) : undefined; switch (await getRealtimeBackend(environment.organizationId, orgFeatureFlags)) { diff --git a/apps/webapp/app/services/realtime/runChangeNotifierInstance.server.ts b/apps/webapp/app/services/realtime/runChangeNotifierInstance.server.ts index 7300d081bf..2e032bd159 100644 --- a/apps/webapp/app/services/realtime/runChangeNotifierInstance.server.ts +++ b/apps/webapp/app/services/realtime/runChangeNotifierInstance.server.ts @@ -14,7 +14,8 @@ function initializeRunChangeNotifier(): RunChangeNotifier { const clusterMode = env.REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_CLUSTER_MODE_ENABLED === "1"; // Sharded pub/sub only works against a cluster; classic pub/sub there would // broadcast every message to every node, so this is what actually shards load. - const shardedPubSub = clusterMode && env.REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_SHARDED_ENABLED === "1"; + const shardedPubSub = + clusterMode && env.REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_SHARDED_ENABLED === "1"; const meter = getMeter("realtime-notifier"); diff --git a/apps/webapp/app/services/realtime/runReader.server.ts b/apps/webapp/app/services/realtime/runReader.server.ts index 98ce4dc35f..952280e774 100644 --- a/apps/webapp/app/services/realtime/runReader.server.ts +++ b/apps/webapp/app/services/realtime/runReader.server.ts @@ -1,4 +1,8 @@ -import { type Prisma, type PrismaClient, type PrismaClientOrTransaction } from "@trigger.dev/database"; +import { + type Prisma, + type PrismaClient, + type PrismaClientOrTransaction, +} from "@trigger.dev/database"; import type { RunStore } from "@internal/run-store"; import { BoundedTtlCache } from "./boundedTtlCache"; import { RESERVED_COLUMNS, type RealtimeRunRow } from "./electricStreamProtocol.server"; diff --git a/apps/webapp/app/services/realtime/s2realtimeStreams.server.ts b/apps/webapp/app/services/realtime/s2realtimeStreams.server.ts index b91f47c6b3..22d2ead23f 100644 --- a/apps/webapp/app/services/realtime/s2realtimeStreams.server.ts +++ b/apps/webapp/app/services/realtime/s2realtimeStreams.server.ts @@ -440,18 +440,15 @@ export class S2RealtimeStreams implements StreamResponder, StreamIngestor { let res: Response; try { - res = await fetch( - `${this.baseUrl}/streams/${encodeURIComponent(s2Stream)}/records?${qs}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${this.token}`, - Accept: "application/json", - "S2-Format": "raw", - "S2-Basin": this.basin, - }, - } - ); + res = await fetch(`${this.baseUrl}/streams/${encodeURIComponent(s2Stream)}/records?${qs}`, { + method: "GET", + headers: { + Authorization: `Bearer ${this.token}`, + Accept: "application/json", + "S2-Format": "raw", + "S2-Basin": this.basin, + }, + }); } catch (err) { this.logger.warn("S2 peek last record: fetch failed", { err, stream: s2Stream }); return false; @@ -569,9 +566,7 @@ export class S2RealtimeStreams implements StreamResponder, StreamIngestor { return (await res.json()) as S2AppendAck; } const text = await res.text().catch(() => ""); - const httpError = new Error( - `S2 append failed: ${res.status} ${res.statusText} ${text}` - ); + const httpError = new Error(`S2 append failed: ${res.status} ${res.statusText} ${text}`); if (res.status >= 400 && res.status < 500) { // 4xx β€” caller-side problem (auth, malformed body, closed stream). // Retrying won't help. diff --git a/apps/webapp/app/services/realtime/sessionRunManager.server.ts b/apps/webapp/app/services/realtime/sessionRunManager.server.ts index b227f382c7..341680b32e 100644 --- a/apps/webapp/app/services/realtime/sessionRunManager.server.ts +++ b/apps/webapp/app/services/realtime/sessionRunManager.server.ts @@ -51,12 +51,7 @@ type EnsureRunForSessionParams = { */ session: Pick< Session, - | "id" - | "friendlyId" - | "taskIdentifier" - | "triggerConfig" - | "currentRunId" - | "currentRunVersion" + "id" | "friendlyId" | "taskIdentifier" | "triggerConfig" | "currentRunId" | "currentRunVersion" >; environment: AuthenticatedEnvironment; reason: EnsureRunReason; @@ -335,12 +330,7 @@ type SwapSessionRunParams = { */ session: Pick< Session, - | "id" - | "friendlyId" - | "taskIdentifier" - | "triggerConfig" - | "currentRunId" - | "currentRunVersion" + "id" | "friendlyId" | "taskIdentifier" | "triggerConfig" | "currentRunId" | "currentRunVersion" >; /** * The run requesting the swap. Optimistic claim requires @@ -382,9 +372,7 @@ export type SwapSessionRunResult = { * a parallel append-time probe that already swapped to a different * run wins the race and `swapped: false` is surfaced. */ -export async function swapSessionRun( - params: SwapSessionRunParams -): Promise { +export async function swapSessionRun(params: SwapSessionRunParams): Promise { const { session, callingRunId, environment, reason, payloadOverrides } = params; // `callingRunId` is the internal cuid (`Session.currentRunId` stores @@ -515,11 +503,7 @@ async function getRunStatusAndFriendlyId( * acceptable degraded behavior. */ async function resolveRunFriendlyId(runId: string): Promise { - const row = await runStore.findRun( - { id: runId }, - { select: { friendlyId: true } }, - $replica - ); + const row = await runStore.findRun({ id: runId }, { select: { friendlyId: true } }, $replica); return row?.friendlyId ?? runId; } diff --git a/apps/webapp/app/services/realtime/sessions.server.ts b/apps/webapp/app/services/realtime/sessions.server.ts index a523111b5b..a7129830e7 100644 --- a/apps/webapp/app/services/realtime/sessions.server.ts +++ b/apps/webapp/app/services/realtime/sessions.server.ts @@ -75,10 +75,7 @@ export function isSessionFriendlyIdForm(value: string): boolean { * Friendlyid-form callers without a matching row are rejected by * the route handler before this is reached. */ -export function canonicalSessionAddressingKey( - row: Session | null, - paramSession: string -): string { +export function canonicalSessionAddressingKey(row: Session | null, paramSession: string): string { if (row) { return row.externalId ?? row.friendlyId; } @@ -126,9 +123,7 @@ export function serializeSession(session: Session): SessionItem { * `$replica` β€” a TaskRun's `friendlyId` is immutable so replica lag is * harmless, and serializing on the writer would just add hot-path load. */ -export async function serializeSessionWithFriendlyRunId( - session: Session -): Promise { +export async function serializeSessionWithFriendlyRunId(session: Session): Promise { const base = serializeSession(session); if (!session.currentRunId) return base; @@ -155,7 +150,9 @@ export async function serializeSessionsWithFriendlyRunIds( sessions: Session[], scope: { projectId: string; runtimeEnvironmentId: string } ): Promise { - const runIds = [...new Set(sessions.map((s) => s.currentRunId).filter((id): id is string => !!id))]; + const runIds = [ + ...new Set(sessions.map((s) => s.currentRunId).filter((id): id is string => !!id)), + ]; // `currentRunId` is a plain string pointer (no FK), so scope the lookup to // the caller's tenant β€” a stale value must not resolve a run in another env. @@ -177,7 +174,7 @@ export async function serializeSessionsWithFriendlyRunIds( return sessions.map((session) => ({ ...serializeSession(session), currentRunId: session.currentRunId - ? friendlyIdByRunId.get(session.currentRunId) ?? null + ? (friendlyIdByRunId.get(session.currentRunId) ?? null) : null, })); } diff --git a/apps/webapp/app/services/realtime/shadowRealtimeClientInstance.server.ts b/apps/webapp/app/services/realtime/shadowRealtimeClientInstance.server.ts index 35333f9639..7453b10783 100644 --- a/apps/webapp/app/services/realtime/shadowRealtimeClientInstance.server.ts +++ b/apps/webapp/app/services/realtime/shadowRealtimeClientInstance.server.ts @@ -37,7 +37,11 @@ function initializeShadowRealtimeClient(): ShadowRealtimeClient { onOutcome: (outcome) => { const { feed } = outcome; if (outcome.serializationMatched) { - compares.add(outcome.serializationMatched, { feed, kind: "serialization", result: "match" }); + compares.add(outcome.serializationMatched, { + feed, + kind: "serialization", + result: "match", + }); } if (outcome.serializationDiverged) { compares.add(outcome.serializationDiverged, { diff --git a/apps/webapp/app/services/realtime/streamBasinProvisioner.server.ts b/apps/webapp/app/services/realtime/streamBasinProvisioner.server.ts index e29aeb168f..3cb30ba6a2 100644 --- a/apps/webapp/app/services/realtime/streamBasinProvisioner.server.ts +++ b/apps/webapp/app/services/realtime/streamBasinProvisioner.server.ts @@ -85,10 +85,7 @@ export async function provisionBasinForOrg( return { kind: "provisioned", basin, retention }; } -export async function reconfigureBasinForOrg( - orgId: string, - retention: string -): Promise { +export async function reconfigureBasinForOrg(orgId: string, retention: string): Promise { if (!isPerOrgBasinsEnabled()) return; const accessToken = env.REALTIME_STREAMS_S2_ACCESS_TOKEN; @@ -121,10 +118,7 @@ type EnsureResult = // Idempotent: provisions if the org has no basin, PATCHes retention if // it does. The single entrypoint the cloud billing app drives β€” both // for the live plan-change path and the bulk backfill. -export async function ensureBasinForOrg( - orgId: string, - retention: string -): Promise { +export async function ensureBasinForOrg(orgId: string, retention: string): Promise { if (!isPerOrgBasinsEnabled()) { return { kind: "skipped", reason: "feature-disabled" }; } @@ -136,9 +130,7 @@ export async function ensureBasinForOrg( if (!org) return { kind: "skipped", reason: "org-not-found" }; if (!org.streamBasinName) { - const result = await provisionBasinForOrg( - { id: org.id, streamBasinName: null, retention } - ); + const result = await provisionBasinForOrg({ id: org.id, streamBasinName: null, retention }); if (result.kind === "provisioned") { return { kind: "provisioned", basin: result.basin, retention: result.retention }; } @@ -245,4 +237,3 @@ async function s2ReconfigureBasin(name: string, opts: ReconfigureBasinOptions): const text = await res.text().catch(() => ""); throw new Error(`S2 reconfigureBasin failed: ${res.status} ${res.statusText} ${text}`); } - diff --git a/apps/webapp/app/services/realtime/types.ts b/apps/webapp/app/services/realtime/types.ts index f09507997a..3121f0e18d 100644 --- a/apps/webapp/app/services/realtime/types.ts +++ b/apps/webapp/app/services/realtime/types.ts @@ -30,11 +30,7 @@ export interface StreamIngestor { getLastChunkIndex(runId: string, streamId: string, clientId: string): Promise; - readRecords( - runId: string, - streamId: string, - afterSeqNum?: number - ): Promise; + readRecords(runId: string, streamId: string, afterSeqNum?: number): Promise; } export type StreamResponseOptions = { diff --git a/apps/webapp/app/services/routeBuilders/apiBuilder.server.ts b/apps/webapp/app/services/routeBuilders/apiBuilder.server.ts index 593c916f7c..49c909f7f7 100644 --- a/apps/webapp/app/services/routeBuilders/apiBuilder.server.ts +++ b/apps/webapp/app/services/routeBuilders/apiBuilder.server.ts @@ -20,10 +20,7 @@ import { API_VERSIONS, getApiVersion } from "~/api/versions"; import { WORKER_HEADERS } from "@trigger.dev/core/v3/runEngineWorker"; import { ServiceValidationError } from "~/v3/services/common.server"; import { EngineServiceValidationError } from "@internal/run-engine"; -import { - tenantContext, - tenantContextFromAuthEnvironment, -} from "~/services/tenantContext.server"; +import { tenantContext, tenantContextFromAuthEnvironment } from "~/services/tenantContext.server"; // Client aborts and service-level validation errors aren't bugs β€” they're // expected at API boundaries. Log them at `warn` so they stay in stdout @@ -148,11 +145,7 @@ function isEveryResource(value: unknown): value is EveryResourceAuth { type AuthResource = RbacResource | AnyResourceAuth | EveryResourceAuth; -function checkAuth( - ability: RbacAbility, - action: string, - resource: AuthResource -): boolean { +function checkAuth(ability: RbacAbility, action: string, resource: AuthResource): boolean { if (isEveryResource(resource)) { // Empty array via [].every() is vacuously true β€” would let any token // pass auth. Routes building everyResource() from request bodies @@ -176,7 +169,7 @@ type ApiKeyRouteBuilderOptions< TParamsSchema extends AnyZodSchema | undefined = undefined, TSearchParamsSchema extends AnyZodSchema | undefined = undefined, THeadersSchema extends AnyZodSchema | undefined = undefined, - TResource = never + TResource = never, > = { params?: TParamsSchema; searchParams?: TSearchParamsSchema; @@ -218,7 +211,7 @@ type ApiKeyHandlerFunction< TParamsSchema extends AnyZodSchema | undefined, TSearchParamsSchema extends AnyZodSchema | undefined, THeadersSchema extends AnyZodSchema | undefined = undefined, - TResource = never + TResource = never, > = (args: { params: TParamsSchema extends z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion ? z.infer @@ -241,7 +234,7 @@ export function createLoaderApiRoute< TParamsSchema extends AnyZodSchema | undefined = undefined, TSearchParamsSchema extends AnyZodSchema | undefined = undefined, THeadersSchema extends AnyZodSchema | undefined = undefined, - TResource = never + TResource = never, >( options: ApiKeyRouteBuilderOptions, handler: ApiKeyHandlerFunction @@ -404,7 +397,7 @@ export function createLoaderApiRoute< type PATRouteBuilderOptions< TParamsSchema extends AnyZodSchema | undefined = undefined, TSearchParamsSchema extends AnyZodSchema | undefined = undefined, - THeadersSchema extends AnyZodSchema | undefined = undefined + THeadersSchema extends AnyZodSchema | undefined = undefined, > = { params?: TParamsSchema; searchParams?: TSearchParamsSchema; @@ -446,7 +439,7 @@ type PATRouteBuilderOptions< type PATHandlerFunction< TParamsSchema extends AnyZodSchema | undefined, TSearchParamsSchema extends AnyZodSchema | undefined, - THeadersSchema extends AnyZodSchema | undefined = undefined + THeadersSchema extends AnyZodSchema | undefined = undefined, > = (args: { params: TParamsSchema extends z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion ? z.infer @@ -468,7 +461,7 @@ type PATHandlerFunction< export function createLoaderPATApiRoute< TParamsSchema extends AnyZodSchema | undefined = undefined, TSearchParamsSchema extends AnyZodSchema | undefined = undefined, - THeadersSchema extends AnyZodSchema | undefined = undefined + THeadersSchema extends AnyZodSchema | undefined = undefined, >( options: PATRouteBuilderOptions, handler: PATHandlerFunction @@ -560,7 +553,10 @@ export function createLoaderPATApiRoute< let authenticationResult: PersonalAccessTokenAuthenticationResult; let ability: RbacAbility; - const bearer = request.headers.get("Authorization")?.replace(/^Bearer /, "").trim(); + const bearer = request.headers + .get("Authorization") + ?.replace(/^Bearer /, "") + .trim(); if (bearer && isUserActorToken(bearer)) { // A user-actor token validates + computes the cap-and-floor ability // in one call, same shape as a PAT. @@ -648,7 +644,7 @@ type ApiKeyActionRouteBuilderOptions< TSearchParamsSchema extends AnyZodSchema | undefined = undefined, THeadersSchema extends AnyZodSchema | undefined = undefined, TBodySchema extends AnyZodSchema | undefined = undefined, - TResource = never + TResource = never, > = { params?: TParamsSchema; searchParams?: TSearchParamsSchema; @@ -701,7 +697,7 @@ type ApiKeyActionHandlerFunction< TSearchParamsSchema extends AnyZodSchema | undefined, THeadersSchema extends AnyZodSchema | undefined = undefined, TBodySchema extends AnyZodSchema | undefined = undefined, - TResource = never + TResource = never, > = (args: { params: TParamsSchema extends z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion ? z.infer @@ -727,7 +723,7 @@ export function createActionApiRoute< TSearchParamsSchema extends AnyZodSchema | undefined = undefined, THeadersSchema extends AnyZodSchema | undefined = undefined, TBodySchema extends AnyZodSchema | undefined = undefined, - TResource = never + TResource = never, >( options: ApiKeyActionRouteBuilderOptions< TParamsSchema, @@ -1002,7 +998,7 @@ type MethodConfig = { type MultiMethodApiRouteOptions< TParamsSchema extends AnyZodSchema | undefined = undefined, TSearchParamsSchema extends AnyZodSchema | undefined = undefined, - THeadersSchema extends AnyZodSchema | undefined = undefined + THeadersSchema extends AnyZodSchema | undefined = undefined, > = { params?: TParamsSchema; searchParams?: TSearchParamsSchema; @@ -1027,7 +1023,7 @@ type MultiMethodApiRouteOptions< export function createMultiMethodApiRoute< TParamsSchema extends AnyZodSchema | undefined = undefined, TSearchParamsSchema extends AnyZodSchema | undefined = undefined, - THeadersSchema extends AnyZodSchema | undefined = undefined + THeadersSchema extends AnyZodSchema | undefined = undefined, >(options: MultiMethodApiRouteOptions) { const { params: paramsSchema, @@ -1243,7 +1239,7 @@ async function wrapResponse( type WorkerLoaderRouteBuilderOptions< TParamsSchema extends AnyZodSchema | undefined = undefined, TSearchParamsSchema extends AnyZodSchema | undefined = undefined, - THeadersSchema extends AnyZodSchema | undefined = undefined + THeadersSchema extends AnyZodSchema | undefined = undefined, > = { params?: TParamsSchema; searchParams?: TSearchParamsSchema; @@ -1253,7 +1249,7 @@ type WorkerLoaderRouteBuilderOptions< type WorkerLoaderHandlerFunction< TParamsSchema extends AnyZodSchema | undefined, TSearchParamsSchema extends AnyZodSchema | undefined, - THeadersSchema extends AnyZodSchema | undefined = undefined + THeadersSchema extends AnyZodSchema | undefined = undefined, > = (args: { params: TParamsSchema extends z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion ? z.infer @@ -1274,7 +1270,7 @@ type WorkerLoaderHandlerFunction< export function createLoaderWorkerApiRoute< TParamsSchema extends AnyZodSchema | undefined = undefined, TSearchParamsSchema extends AnyZodSchema | undefined = undefined, - THeadersSchema extends AnyZodSchema | undefined = undefined + THeadersSchema extends AnyZodSchema | undefined = undefined, >( options: WorkerLoaderRouteBuilderOptions, handler: WorkerLoaderHandlerFunction @@ -1360,7 +1356,7 @@ type WorkerActionRouteBuilderOptions< TParamsSchema extends AnyZodSchema | undefined = undefined, TSearchParamsSchema extends AnyZodSchema | undefined = undefined, THeadersSchema extends AnyZodSchema | undefined = undefined, - TBodySchema extends AnyZodSchema | undefined = undefined + TBodySchema extends AnyZodSchema | undefined = undefined, > = { params?: TParamsSchema; searchParams?: TSearchParamsSchema; @@ -1373,7 +1369,7 @@ type WorkerActionHandlerFunction< TParamsSchema extends AnyZodSchema | undefined, TSearchParamsSchema extends AnyZodSchema | undefined, THeadersSchema extends AnyZodSchema | undefined = undefined, - TBodySchema extends AnyZodSchema | undefined = undefined + TBodySchema extends AnyZodSchema | undefined = undefined, > = (args: { params: TParamsSchema extends z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion ? z.infer @@ -1398,7 +1394,7 @@ export function createActionWorkerApiRoute< TParamsSchema extends AnyZodSchema | undefined = undefined, TSearchParamsSchema extends AnyZodSchema | undefined = undefined, THeadersSchema extends AnyZodSchema | undefined = undefined, - TBodySchema extends AnyZodSchema | undefined = undefined + TBodySchema extends AnyZodSchema | undefined = undefined, >( options: WorkerActionRouteBuilderOptions< TParamsSchema, diff --git a/apps/webapp/app/services/routeBuilders/dashboardBuilder.ts b/apps/webapp/app/services/routeBuilders/dashboardBuilder.ts index b009c21842..a460389591 100644 --- a/apps/webapp/app/services/routeBuilders/dashboardBuilder.ts +++ b/apps/webapp/app/services/routeBuilders/dashboardBuilder.ts @@ -83,7 +83,7 @@ export function dashboardLoader< TParams extends AnyZodSchema | undefined = undefined, TSearchParams extends AnyZodSchema | undefined = undefined, TContext extends AuthScope = AuthScope, - TReturn extends Response = Response + TReturn extends Response = Response, >( options: DashboardLoaderOptions, handler: (args: DashboardLoaderHandlerArgs) => Promise @@ -109,7 +109,7 @@ export function dashboardLoader< export type DashboardActionOptions< TParams, TSearchParams, - TContext extends AuthScope + TContext extends AuthScope, > = DashboardLoaderOptions; export type DashboardActionHandlerArgs = @@ -119,7 +119,7 @@ export function dashboardAction< TParams extends AnyZodSchema | undefined = undefined, TSearchParams extends AnyZodSchema | undefined = undefined, TContext extends AuthScope = AuthScope, - TReturn extends Response = Response + TReturn extends Response = Response, >( options: DashboardActionOptions, handler: (args: DashboardActionHandlerArgs) => Promise diff --git a/apps/webapp/app/services/runsReplicationService.server.ts b/apps/webapp/app/services/runsReplicationService.server.ts index 31d8a3844c..f76a3c7b83 100644 --- a/apps/webapp/app/services/runsReplicationService.server.ts +++ b/apps/webapp/app/services/runsReplicationService.server.ts @@ -101,7 +101,7 @@ type TaskRunInsert = { export type RunsReplicationServiceEvents = { message: [{ lsn: string; message: PgoutputMessage; service: RunsReplicationService }]; batchFlushed: [ - { flushId: string; taskRunInserts: TaskRunInsertArray[]; payloadInserts: PayloadInsertArray[] } + { flushId: string; taskRunInserts: TaskRunInsertArray[]; payloadInserts: PayloadInsertArray[] }, ]; }; diff --git a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts index d32652a0b3..9bf6731477 100644 --- a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts @@ -260,7 +260,7 @@ export class ClickHouseRunsRepository implements IRunsRepository { environmentId: options.environmentId, }); - const periodMs = options.period ? parseDuration(options.period) ?? undefined : undefined; + const periodMs = options.period ? (parseDuration(options.period) ?? undefined) : undefined; if (periodMs) { queryBuilder.where("created_at >= fromUnixTimestamp64Milli({period: Int64})", { period: new Date(Date.now() - periodMs).getTime(), @@ -405,9 +405,7 @@ function applyRunFiltersToQueryBuilder( if (options.taskKinds && options.taskKinds.length > 0) { const includesStandard = options.taskKinds.includes("STANDARD"); // Include empty string when filtering for STANDARD (default value for pre-existing runs) - const effectiveKinds = includesStandard - ? [...options.taskKinds, ""] - : options.taskKinds; + const effectiveKinds = includesStandard ? [...options.taskKinds, ""] : options.taskKinds; if (effectiveKinds.length === 1) { queryBuilder.where("task_kind = {taskKind: String}", { diff --git a/apps/webapp/app/services/runsRepository/runsRepository.server.ts b/apps/webapp/app/services/runsRepository/runsRepository.server.ts index d40e5cf8c0..a4543a26df 100644 --- a/apps/webapp/app/services/runsRepository/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/runsRepository.server.ts @@ -274,7 +274,7 @@ export async function convertRunListInputOptionsToFilterRunsOptions( from: options.from, to: options.to, }); - convertedOptions.period = time.period ? parseDuration(time.period) ?? undefined : undefined; + convertedOptions.period = time.period ? (parseDuration(time.period) ?? undefined) : undefined; // Batch friendlyId to id if (options.batchId && options.batchId.startsWith("batch_")) { diff --git a/apps/webapp/app/services/secrets/secretStore.server.ts b/apps/webapp/app/services/secrets/secretStore.server.ts index 58af8e86f7..10405f975a 100644 --- a/apps/webapp/app/services/secrets/secretStore.server.ts +++ b/apps/webapp/app/services/secrets/secretStore.server.ts @@ -219,7 +219,7 @@ class PrismaSecretStore implements SecretStoreProvider { export function getSecretStore< K extends SecretStoreOptions, - TOptions extends ProviderInitializationOptions[K] + TOptions extends ProviderInitializationOptions[K], >(provider: K, options?: TOptions): SecretStore { switch (provider) { case "DATABASE": { diff --git a/apps/webapp/app/services/sessionStreamWaitpointCache.server.ts b/apps/webapp/app/services/sessionStreamWaitpointCache.server.ts index 3112167828..7b53042d8d 100644 --- a/apps/webapp/app/services/sessionStreamWaitpointCache.server.ts +++ b/apps/webapp/app/services/sessionStreamWaitpointCache.server.ts @@ -87,13 +87,7 @@ export async function addSessionStreamWaitpoint( try { const key = buildKey(environmentId, addressingKey, io); - await redis.eval( - ADD_WAITPOINT_SCRIPT, - 1, - key, - waitpointId, - String(ttlMs ?? DEFAULT_TTL_MS) - ); + await redis.eval(ADD_WAITPOINT_SCRIPT, 1, key, waitpointId, String(ttlMs ?? DEFAULT_TTL_MS)); } catch (error) { logger.error("Failed to set session stream waitpoint cache", { environmentId, diff --git a/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts b/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts index b66faf4d3e..10086c52f3 100644 --- a/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts +++ b/apps/webapp/app/services/sessionsRepository/clickhouseSessionsRepository.server.ts @@ -56,7 +56,7 @@ export class ClickHouseSessionsRepository implements ISessionsRepository { const direction = options.page.direction ?? "forward"; switch (direction) { case "forward": { - previousCursor = options.page.cursor ? sessionIds.at(0) ?? null : null; + previousCursor = options.page.cursor ? (sessionIds.at(0) ?? null) : null; if (hasMore) { nextCursor = sessionIds[options.page.size - 1]; } @@ -155,7 +155,7 @@ export class ClickHouseSessionsRepository implements ISessionsRepository { environmentId: options.environmentId, }); - const periodMs = options.period ? parseDuration(options.period) ?? undefined : undefined; + const periodMs = options.period ? (parseDuration(options.period) ?? undefined) : undefined; if (periodMs) { queryBuilder.where("created_at >= fromUnixTimestamp64Milli({period: Int64})", { period: new Date(Date.now() - periodMs).getTime(), @@ -221,9 +221,7 @@ function applySessionFiltersToQueryBuilder( if (options.statuses && options.statuses.length > 0) { const conditions: string[] = []; if (options.statuses.includes("ACTIVE")) { - conditions.push( - "(closed_at IS NULL AND (expires_at IS NULL OR expires_at > now64(3)))" - ); + conditions.push("(closed_at IS NULL AND (expires_at IS NULL OR expires_at > now64(3)))"); } if (options.statuses.includes("CLOSED")) { conditions.push("closed_at IS NOT NULL"); diff --git a/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts b/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts index 5a80805103..4c15d0423b 100644 --- a/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts +++ b/apps/webapp/app/services/sessionsRepository/sessionsRepository.server.ts @@ -202,6 +202,6 @@ export function convertSessionListInputOptionsToFilterOptions( ): FilterSessionsOptions { return { ...options, - period: options.period ? parseDuration(options.period) ?? undefined : undefined, + period: options.period ? (parseDuration(options.period) ?? undefined) : undefined, }; } diff --git a/apps/webapp/app/services/signals.server.ts b/apps/webapp/app/services/signals.server.ts index 308f16fdea..b20df4ebdf 100644 --- a/apps/webapp/app/services/signals.server.ts +++ b/apps/webapp/app/services/signals.server.ts @@ -6,13 +6,13 @@ export type SignalsEvents = { { time: Date; signal: NodeJS.Signals; - } + }, ]; SIGINT: [ { time: Date; signal: NodeJS.Signals; - } + }, ]; }; diff --git a/apps/webapp/app/services/ssoSessionRevalidation.server.ts b/apps/webapp/app/services/ssoSessionRevalidation.server.ts index 2d582dac1a..a8f2cb15c0 100644 --- a/apps/webapp/app/services/ssoSessionRevalidation.server.ts +++ b/apps/webapp/app/services/ssoSessionRevalidation.server.ts @@ -77,7 +77,9 @@ export async function revalidateSsoSession( // `sso.revalidation.timeout` warn for alerting. const timeoutMs = env.SSO_SESSION_REVALIDATION_TIMEOUT_MS; let timer: ReturnType | undefined; - let result: Awaited> | typeof REVALIDATION_TIMEOUT; + let result: + | Awaited> + | typeof REVALIDATION_TIMEOUT; try { result = await Promise.race([ // ResultAsync is a PromiseLike; Promise.resolve unwraps it to a Result. diff --git a/apps/webapp/app/services/taskIdentifierRegistry.server.ts b/apps/webapp/app/services/taskIdentifierRegistry.server.ts index 074c9b4c3b..6a1842b523 100644 --- a/apps/webapp/app/services/taskIdentifierRegistry.server.ts +++ b/apps/webapp/app/services/taskIdentifierRegistry.server.ts @@ -1,4 +1,8 @@ -import { type PrismaClient, type PrismaClientOrTransaction, TaskTriggerSource } from "@trigger.dev/database"; +import { + type PrismaClient, + type PrismaClientOrTransaction, + TaskTriggerSource, +} from "@trigger.dev/database"; import { $replica, prisma } from "~/db.server"; import { getAllTaskIdentifiers } from "~/models/task.server"; import { logger } from "./logger.server"; @@ -99,8 +103,7 @@ export async function syncTaskIdentifiers( function sortEntries(entries: TaskIdentifierEntry[]): TaskIdentifierEntry[] { return entries.sort((a, b) => { - if (a.isInLatestDeployment !== b.isInLatestDeployment) - return a.isInLatestDeployment ? -1 : 1; + if (a.isInLatestDeployment !== b.isInLatestDeployment) return a.isInLatestDeployment ? -1 : 1; return a.slug.localeCompare(b.slug); }); } diff --git a/apps/webapp/app/services/telemetry.server.ts b/apps/webapp/app/services/telemetry.server.ts index ad67871c19..71ca0a0e66 100644 --- a/apps/webapp/app/services/telemetry.server.ts +++ b/apps/webapp/app/services/telemetry.server.ts @@ -47,11 +47,11 @@ class Telemetry { createdAt: user.createdAt, isNewUser, }; - + if (referralSource) { properties.referralSource = referralSource; } - + this.#posthogClient.identify({ distinctId: user.id, properties, diff --git a/apps/webapp/app/services/unkey/redisCacheStore.server.ts b/apps/webapp/app/services/unkey/redisCacheStore.server.ts index 25bbb27634..0bc88a9af9 100644 --- a/apps/webapp/app/services/unkey/redisCacheStore.server.ts +++ b/apps/webapp/app/services/unkey/redisCacheStore.server.ts @@ -8,9 +8,10 @@ export type RedisCacheStoreConfig = { name?: string; }; -export class RedisCacheStore - implements Store -{ +export class RedisCacheStore implements Store< + TNamespace, + TValue +> { public readonly name = "redis"; private readonly redis: RedisClient; diff --git a/apps/webapp/app/services/vercelIntegration.server.ts b/apps/webapp/app/services/vercelIntegration.server.ts index 286e997405..326d43e583 100644 --- a/apps/webapp/app/services/vercelIntegration.server.ts +++ b/apps/webapp/app/services/vercelIntegration.server.ts @@ -44,7 +44,7 @@ export class VercelIntegrationService { } async getVercelProjectIntegration( - projectId: string, + projectId: string ): Promise { const integration = await this.#prismaClient.organizationProjectIntegration.findFirst({ where: { @@ -107,7 +107,9 @@ export class VercelIntegrationService { return integrations .map((integration) => { - const parsedData = VercelProjectIntegrationDataSchema.safeParse(integration.integrationData); + const parsedData = VercelProjectIntegrationDataSchema.safeParse( + integration.integrationData + ); if (!parsedData.success) { logger.error("Failed to parse Vercel integration data", { integrationId: integration.id, @@ -199,9 +201,7 @@ export class VercelIntegrationService { }); if (existing) { - const parsedData = VercelProjectIntegrationDataSchema.safeParse( - existing.integrationData - ); + const parsedData = VercelProjectIntegrationDataSchema.safeParse(existing.integrationData); const updated = await tx.organizationProjectIntegration.update({ where: { id: existing.id }, @@ -270,14 +270,15 @@ export class VercelIntegrationService { : { success: false, errors: [syncResultAsync.error.message] }; if (wasCreated) { - const disableResult = await VercelIntegrationRepository.getVercelClient(orgIntegration) - .andThen((client) => - VercelIntegrationRepository.disableAutoAssignCustomDomains( - client, - params.vercelProjectId, - teamId - ) - ); + const disableResult = await VercelIntegrationRepository.getVercelClient( + orgIntegration + ).andThen((client) => + VercelIntegrationRepository.disableAutoAssignCustomDomains( + client, + params.vercelProjectId, + teamId + ) + ); if (disableResult.isErr()) { logger.warn("Failed to disable autoAssignCustomDomains during project selection", { @@ -329,9 +330,8 @@ export class VercelIntegrationService { return { ...updated, parsedIntegrationData: updatedData }; } - const orgIntegration = await VercelIntegrationRepository.findVercelOrgIntegrationForProject( - projectId - ); + const orgIntegration = + await VercelIntegrationRepository.findVercelOrgIntegrationForProject(projectId); if (orgIntegration) { await this.#syncTriggerVersionToVercelProduction( @@ -377,11 +377,14 @@ export class VercelIntegrationService { }); if (removeResult.isErr()) { - logger.error("Failed to remove staging TRIGGER_SECRET_KEY from previous custom environment", { - projectId, - previousCustomEnvironmentId, - error: removeResult.error.message, - }); + logger.error( + "Failed to remove staging TRIGGER_SECRET_KEY from previous custom environment", + { + projectId, + previousCustomEnvironmentId, + error: removeResult.error.message, + } + ); } } @@ -534,7 +537,12 @@ export class VercelIntegrationService { return null; } - const syncEnvVarsMapping = params.syncEnvVarsMapping ?? { "dev":{}, "stg":{}, "prod":{}, "preview":{} }; + const syncEnvVarsMapping = params.syncEnvVarsMapping ?? { + dev: {}, + stg: {}, + prod: {}, + preview: {}, + }; const updatedData: VercelProjectIntegrationData = { ...existing.parsedIntegrationData, config: { @@ -557,9 +565,8 @@ export class VercelIntegrationService { }, }); - const orgIntegration = await VercelIntegrationRepository.findVercelOrgIntegrationForProject( - projectId - ); + const orgIntegration = + await VercelIntegrationRepository.findVercelOrgIntegrationForProject(projectId); if (orgIntegration) { const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration); @@ -702,7 +709,10 @@ export class VercelIntegrationService { logger.error("Failed to sync TRIGGER_VERSION to Vercel production", { projectId, vercelProjectId, - error: createResult.error instanceof Error ? createResult.error.message : String(createResult.error), + error: + createResult.error instanceof Error + ? createResult.error.message + : String(createResult.error), }); return; } @@ -826,4 +836,3 @@ export class VercelIntegrationService { return true; } } - diff --git a/apps/webapp/app/utils.ts b/apps/webapp/app/utils.ts index 2f8cdf7e50..680c2a0c9f 100644 --- a/apps/webapp/app/utils.ts +++ b/apps/webapp/app/utils.ts @@ -83,10 +83,13 @@ export function useMatchesData(id: string | string[], debug: boolean = false): U 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); + const route = paths.reduce( + (acc, path) => { + if (acc) return acc; + return matchingRoutes.find((route) => route.id === path); + }, + undefined as UIMatch | undefined + ); return route; } diff --git a/apps/webapp/app/utils/branchableEnvironment.ts b/apps/webapp/app/utils/branchableEnvironment.ts index 53ec64bc5f..997042c316 100644 --- a/apps/webapp/app/utils/branchableEnvironment.ts +++ b/apps/webapp/app/utils/branchableEnvironment.ts @@ -6,10 +6,7 @@ type BranchableEnvironmentInput = { isBranchableEnvironment: boolean; }; -export type BranchableEnvironmentType = Extract< - RuntimeEnvironmentType, - "PREVIEW" | "DEVELOPMENT" ->; +export type BranchableEnvironmentType = Extract; /** * The wire/form token for a branchable environment kind, as sent by the CLI and @@ -21,8 +18,10 @@ export function toBranchableEnvironmentType( env: BranchableEnvironmentToken ): BranchableEnvironmentType { switch (env) { - case "preview": return "PREVIEW"; - case "development": return "DEVELOPMENT"; + case "preview": + return "PREVIEW"; + case "development": + return "DEVELOPMENT"; } } diff --git a/apps/webapp/app/utils/dataExport.ts b/apps/webapp/app/utils/dataExport.ts index be7cde397b..c58d11a7e5 100644 --- a/apps/webapp/app/utils/dataExport.ts +++ b/apps/webapp/app/utils/dataExport.ts @@ -33,7 +33,10 @@ function escapeCSVValue(value: unknown): string { * @param columns - Column metadata describing the result columns * @returns CSV string with header row and data rows */ -export function rowsToCSV(rows: Record[], columns: OutputColumnMetadata[]): string { +export function rowsToCSV( + rows: Record[], + columns: OutputColumnMetadata[] +): string { if (columns.length === 0) { return ""; } @@ -44,7 +47,9 @@ export function rowsToCSV(rows: Record[], columns: OutputColumn const headerRow = columnNames.map(escapeCSVValue).join(","); // Data rows - const dataRows = rows.map((row) => columnNames.map((name) => escapeCSVValue(row[name])).join(",")); + const dataRows = rows.map((row) => + columnNames.map((name) => escapeCSVValue(row[name])).join(",") + ); return [headerRow, ...dataRows].join("\n"); } @@ -75,5 +80,3 @@ export function downloadFile(content: string, filename: string, mimeType: string a.click(); URL.revokeObjectURL(url); } - - diff --git a/apps/webapp/app/utils/logUtils.ts b/apps/webapp/app/utils/logUtils.ts index 71ec44534b..b6f3681d0d 100644 --- a/apps/webapp/app/utils/logUtils.ts +++ b/apps/webapp/app/utils/logUtils.ts @@ -54,9 +54,7 @@ export function highlightSearchText( parts.push(text.substring(lastIndex, match.index)); } // Add highlighted match - parts.push( - createElement("span", { key: `match-${matchCount}`, style }, match[0]) - ); + parts.push(createElement("span", { key: `match-${matchCount}`, style }, match[0])); lastIndex = regex.lastIndex; matchCount++; } diff --git a/apps/webapp/app/utils/longPollingFetch.ts b/apps/webapp/app/utils/longPollingFetch.ts index cb5f97693b..708734dcb4 100644 --- a/apps/webapp/app/utils/longPollingFetch.ts +++ b/apps/webapp/app/utils/longPollingFetch.ts @@ -52,7 +52,9 @@ export async function longPollingFetch( // ReadableStream stays open and undici keeps buffering chunks into memory // until the upstream times out (see H1 isolation test β€” ~44 KB retained // per unconsumed-body fetch in RSS). - try { await upstream?.body?.cancel(); } catch {} + try { + await upstream?.body?.cancel(); + } catch {} // AbortError is the expected path when downstream disconnects with a // propagated signal β€” treat as a clean client-close, not a server error. diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index 3b65fde139..ca167e2163 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -733,7 +733,6 @@ export function branchesDevPath( return `${v3EnvironmentPath(organization, project, environment)}/dev-branches`; } - export function concurrencyPath( organization: OrgForPath, project: ProjectForPath, diff --git a/apps/webapp/app/utils/plain.server.ts b/apps/webapp/app/utils/plain.server.ts index ee205374e1..72b2058843 100644 --- a/apps/webapp/app/utils/plain.server.ts +++ b/apps/webapp/app/utils/plain.server.ts @@ -10,14 +10,7 @@ type Input = { labelTypeIds?: string[]; }; -export async function sendToPlain({ - userId, - email, - name, - title, - components, - labelTypeIds, -}: Input) { +export async function sendToPlain({ userId, email, name, title, components, labelTypeIds }: Input) { if (!env.PLAIN_API_KEY) { return; } diff --git a/apps/webapp/app/utils/prismaErrors.ts b/apps/webapp/app/utils/prismaErrors.ts index ddbdb3cef6..6c128b259d 100644 --- a/apps/webapp/app/utils/prismaErrors.ts +++ b/apps/webapp/app/utils/prismaErrors.ts @@ -117,7 +117,11 @@ export function logTransactionInfrastructureError( error: unknown, log: ErrorLogger = logger ): boolean { - if (!isInfrastructureError(error) || isPrismaKnownError(error) || infraErrorAlreadyLogged(error)) { + if ( + !isInfrastructureError(error) || + isPrismaKnownError(error) || + infraErrorAlreadyLogged(error) + ) { return false; } diff --git a/apps/webapp/app/utils/reloadingRegistry.server.ts b/apps/webapp/app/utils/reloadingRegistry.server.ts index 65a6585012..81eb672345 100644 --- a/apps/webapp/app/utils/reloadingRegistry.server.ts +++ b/apps/webapp/app/utils/reloadingRegistry.server.ts @@ -55,7 +55,9 @@ export type ReloadingRegistryOptions = { * contract as the datastore / LLM-pricing registries. Interval-only: no pub/sub * (a follow-up if sub-second propagation is ever needed). */ -export function createReloadingRegistry(opts: ReloadingRegistryOptions): ReloadingRegistry { +export function createReloadingRegistry( + opts: ReloadingRegistryOptions +): ReloadingRegistry { let snapshot: T | undefined; let loaded = false; let loadSeq = 0; diff --git a/apps/webapp/app/utils/searchParams.ts b/apps/webapp/app/utils/searchParams.ts index bc2d7d1e47..739b284c2a 100644 --- a/apps/webapp/app/utils/searchParams.ts +++ b/apps/webapp/app/utils/searchParams.ts @@ -54,7 +54,10 @@ export function objectToSearchParams( } class SearchParams { - constructor(private params: TParams, readonly schema: ZodType) {} + constructor( + private params: TParams, + readonly schema: ZodType + ) {} get(key: keyof TParams) { return this.params[key]; diff --git a/apps/webapp/app/utils/timeGranularity.ts b/apps/webapp/app/utils/timeGranularity.ts index 0d8592d27b..0c8ecc41ac 100644 --- a/apps/webapp/app/utils/timeGranularity.ts +++ b/apps/webapp/app/utils/timeGranularity.ts @@ -1,15 +1,13 @@ import { z } from "zod"; import parseDuration from "parse-duration"; -const DurationString = z - .string() - .refine( - (val) => { - const ms = parseDuration(val); - return ms !== null && ms > 0; - }, - (val) => ({ message: `Invalid or non-positive duration string: "${val}"` }) - ); +const DurationString = z.string().refine( + (val) => { + const ms = parseDuration(val); + return ms !== null && ms > 0; + }, + (val) => ({ message: `Invalid or non-positive duration string: "${val}"` }) +); const BracketSchema = z.object({ max: z.union([z.literal("Infinity"), DurationString]), diff --git a/apps/webapp/app/v3/accountsWebhookWorker.server.ts b/apps/webapp/app/v3/accountsWebhookWorker.server.ts index 0caf5c591f..0c56968102 100644 --- a/apps/webapp/app/v3/accountsWebhookWorker.server.ts +++ b/apps/webapp/app/v3/accountsWebhookWorker.server.ts @@ -79,9 +79,7 @@ function initializeWorker() { // Only poll on worker-role instances (same gate as commonWorker) and // only when the feature is enabled (no plugin loaded otherwise). if (env.COMMON_WORKER_ENABLED === "true" && env.SSO_ENABLED) { - logger.debug( - `πŸ‘¨β€πŸ­ Starting accounts webhook worker at host ${env.COMMON_WORKER_REDIS_HOST}` - ); + logger.debug(`πŸ‘¨β€πŸ­ Starting accounts webhook worker at host ${env.COMMON_WORKER_REDIS_HOST}`); worker.start(); } diff --git a/apps/webapp/app/v3/dynamicFlushScheduler.server.ts b/apps/webapp/app/v3/dynamicFlushScheduler.server.ts index afa3e13322..ddf11c2103 100644 --- a/apps/webapp/app/v3/dynamicFlushScheduler.server.ts +++ b/apps/webapp/app/v3/dynamicFlushScheduler.server.ts @@ -420,4 +420,4 @@ export class DynamicFlushScheduler { await new Promise((resolve) => setTimeout(resolve, 100)); } } -} \ No newline at end of file +} diff --git a/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts b/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts index 3bd9d90356..39a8025213 100644 --- a/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts +++ b/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts @@ -197,8 +197,7 @@ export class EnvironmentVariablesRepository implements Repository { existingSecret && existingSecret.secret === variable.value && existingValueRecord && - (options.isSecret === undefined || - existingValueRecord.isSecret === options.isSecret); + (options.isSecret === undefined || existingValueRecord.isSecret === options.isSecret); if (canSkip) { continue; } diff --git a/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts b/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts index 1e091944e6..79fe5a4f9d 100644 --- a/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts +++ b/apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts @@ -889,7 +889,7 @@ export class ClickhouseEventRepository implements IEventRepository { const traceId = options.spanParentAsLink ? generateTraceId() - : propagatedContext?.traceparent?.traceId ?? generateTraceId(); + : (propagatedContext?.traceparent?.traceId ?? generateTraceId()); const parentId = options.spanParentAsLink ? undefined : propagatedContext?.traceparent?.spanId; const spanId = options.spanIdSeed ? generateDeterministicSpanId(traceId, options.spanIdSeed) diff --git a/apps/webapp/app/v3/eventRepository/eventRepository.server.ts b/apps/webapp/app/v3/eventRepository/eventRepository.server.ts index 268c955fd2..46ca8cbe48 100644 --- a/apps/webapp/app/v3/eventRepository/eventRepository.server.ts +++ b/apps/webapp/app/v3/eventRepository/eventRepository.server.ts @@ -500,8 +500,8 @@ export class EventRepository implements IEventRepository { const isError = isCancelled ? false : typeof overrides?.isError === "boolean" - ? overrides.isError - : event.isError; + ? overrides.isError + : event.isError; const span = { id: event.spanId, @@ -632,8 +632,8 @@ export class EventRepository implements IEventRepository { const isError = isCancelled ? false : typeof overrides?.isError === "boolean" - ? overrides.isError - : event.isError; + ? overrides.isError + : event.isError; const properties = event.properties ? removePrivateProperties(event.properties as Attributes) @@ -700,7 +700,7 @@ export class EventRepository implements IEventRepository { { includeDebugLogs: options?.includeDebugLogs } )) { const properties = event.properties - ? removePrivateProperties(event.properties as Attributes) ?? {} + ? (removePrivateProperties(event.properties as Attributes) ?? {}) : {}; yield { @@ -932,8 +932,8 @@ export class EventRepository implements IEventRepository { const isError = isCancelled ? false : typeof overrides?.isError === "boolean" - ? overrides.isError - : event.isError; + ? overrides.isError + : event.isError; const span = { id: event.spanId, @@ -1199,7 +1199,7 @@ export class EventRepository implements IEventRepository { const traceId = options.spanParentAsLink ? generateTraceId() - : propagatedContext?.traceparent?.traceId ?? generateTraceId(); + : (propagatedContext?.traceparent?.traceId ?? generateTraceId()); const parentId = options.spanParentAsLink ? undefined : propagatedContext?.traceparent?.spanId; const tracestate = options.spanParentAsLink ? undefined : propagatedContext?.tracestate; const spanId = options.spanIdSeed diff --git a/apps/webapp/app/v3/eventRepository/index.server.ts b/apps/webapp/app/v3/eventRepository/index.server.ts index c59be0f3f5..ca0fb85f00 100644 --- a/apps/webapp/app/v3/eventRepository/index.server.ts +++ b/apps/webapp/app/v3/eventRepository/index.server.ts @@ -29,10 +29,7 @@ export type EventStoreType = (typeof EVENT_STORE_TYPES)[keyof typeof EVENT_STORE * directly and gate startup on `clickhouseFactory.isReady()`. Everything else * should use {@link getEventRepositoryForStore}, the async variant below. */ -function resolveEventRepositoryForStore( - store: string, - organizationId: string -): IEventRepository { +function resolveEventRepositoryForStore(store: string, organizationId: string): IEventRepository { if (store === EVENT_STORE_TYPES.CLICKHOUSE || store === EVENT_STORE_TYPES.CLICKHOUSE_V2) { return clickhouseFactory.getEventRepositoryForOrganizationSync(store, organizationId) .repository; diff --git a/apps/webapp/app/v3/eventRepository/traceExport.server.ts b/apps/webapp/app/v3/eventRepository/traceExport.server.ts index d1e9f5b6f0..c0a736c60b 100644 --- a/apps/webapp/app/v3/eventRepository/traceExport.server.ts +++ b/apps/webapp/app/v3/eventRepository/traceExport.server.ts @@ -105,7 +105,8 @@ const logFormat: TraceExportFormat = { const level = event.level.padEnd(5); const errMsg = errorMessage(event); const status = event.isError ? (errMsg ? ` [ERROR: ${errMsg}]` : " [ERROR]") : ""; - const duration = event.durationNs > 0 ? ` (${formatDurationNanoseconds(event.durationNs)})` : ""; + const duration = + event.durationNs > 0 ? ` (${formatDurationNanoseconds(event.durationNs)})` : ""; let out = `${time} ${level} [${lineage(event)}] ${event.message}${status}${duration}\n`; if (hasProperties(event.propertiesText)) { diff --git a/apps/webapp/app/v3/legacyRunEngineWorker.server.ts b/apps/webapp/app/v3/legacyRunEngineWorker.server.ts index 7a381063bd..7e4458b017 100644 --- a/apps/webapp/app/v3/legacyRunEngineWorker.server.ts +++ b/apps/webapp/app/v3/legacyRunEngineWorker.server.ts @@ -5,10 +5,7 @@ import { env } from "~/env.server"; import { logger } from "~/services/logger.server"; import { singleton } from "~/utils/singleton"; import { TaskRunHeartbeatFailedService } from "./taskRunHeartbeatFailed.server"; -import { - completeBatchTaskRunItemV3, - tryCompleteBatchV3, -} from "./services/batchTriggerV3.server"; +import { completeBatchTaskRunItemV3, tryCompleteBatchV3 } from "./services/batchTriggerV3.server"; import { prisma } from "~/db.server"; import { marqs } from "./marqs/index.server"; diff --git a/apps/webapp/app/v3/marqs/asyncWorker.server.ts b/apps/webapp/app/v3/marqs/asyncWorker.server.ts index 016662e1d5..64171f3665 100644 --- a/apps/webapp/app/v3/marqs/asyncWorker.server.ts +++ b/apps/webapp/app/v3/marqs/asyncWorker.server.ts @@ -2,7 +2,10 @@ export class AsyncWorker { private running = false; private timeout?: NodeJS.Timeout; - constructor(private readonly fn: () => Promise, private readonly interval: number) {} + constructor( + private readonly fn: () => Promise, + private readonly interval: number + ) {} start() { if (this.running) { diff --git a/apps/webapp/app/v3/marqs/devQueueConsumer.server.ts b/apps/webapp/app/v3/marqs/devQueueConsumer.server.ts index 3143e40f0d..3b6c1f4448 100644 --- a/apps/webapp/app/v3/marqs/devQueueConsumer.server.ts +++ b/apps/webapp/app/v3/marqs/devQueueConsumer.server.ts @@ -398,8 +398,8 @@ export class DevQueueConsumer { } const backgroundWorker = existingTaskRun.lockedToVersionId - ? this._deprecatedWorkers.get(existingTaskRun.lockedToVersionId) ?? - this._backgroundWorkers.get(existingTaskRun.lockedToVersionId) + ? (this._deprecatedWorkers.get(existingTaskRun.lockedToVersionId) ?? + this._backgroundWorkers.get(existingTaskRun.lockedToVersionId)) : this.#getLatestBackgroundWorker(); if (!backgroundWorker) { diff --git a/apps/webapp/app/v3/marqs/fairDequeuingStrategy.server.ts b/apps/webapp/app/v3/marqs/fairDequeuingStrategy.server.ts index e9205cd000..1e88a5348b 100644 --- a/apps/webapp/app/v3/marqs/fairDequeuingStrategy.server.ts +++ b/apps/webapp/app/v3/marqs/fairDequeuingStrategy.server.ts @@ -222,13 +222,16 @@ export class FairDequeuingStrategy implements MarQSFairDequeueStrategy { } #orderQueuesByEnvs(envs: string[], snapshot: FairQueueSnapshot): Array { - const queuesByEnv = snapshot.queues.reduce((acc, queue) => { - if (!acc[queue.env]) { - acc[queue.env] = []; - } - acc[queue.env].push(queue); - return acc; - }, {} as Record>); + const queuesByEnv = snapshot.queues.reduce( + (acc, queue) => { + if (!acc[queue.env]) { + acc[queue.env] = []; + } + acc[queue.env].push(queue); + return acc; + }, + {} as Record> + ); return envs.reduce((acc, envId) => { if (queuesByEnv[envId]) { @@ -391,12 +394,15 @@ export class FairDequeuingStrategy implements MarQSFairDequeueStrategy { const envIdsAtFullConcurrency = new Set(envsAtFullConcurrency.map((env) => env.id)); - const envsSnapshot = envs.reduce((acc, env) => { - if (!envIdsAtFullConcurrency.has(env.id)) { - acc[env.id] = env; - } - return acc; - }, {} as Record); + const envsSnapshot = envs.reduce( + (acc, env) => { + if (!envIdsAtFullConcurrency.has(env.id)) { + acc[env.id] = env; + } + return acc; + }, + {} as Record + ); span.setAttributes({ env_count: envs.length, @@ -427,13 +433,16 @@ export class FairDequeuingStrategy implements MarQSFairDequeueStrategy { #selectTopEnvs(queues: FairQueue[], maximumEnvCount: number): Set { // Group queues by env - const queuesByEnv = queues.reduce((acc, queue) => { - if (!acc[queue.env]) { - acc[queue.env] = []; - } - acc[queue.env].push(queue); - return acc; - }, {} as Record); + const queuesByEnv = queues.reduce( + (acc, queue) => { + if (!acc[queue.env]) { + acc[queue.env] = []; + } + acc[queue.env].push(queue); + return acc; + }, + {} as Record + ); // Calculate average age for each env const envAverageAges = Object.entries(queuesByEnv).map(([envId, envQueues]) => { diff --git a/apps/webapp/app/v3/marqs/index.server.ts b/apps/webapp/app/v3/marqs/index.server.ts index 5348f228ae..c1beced9ae 100644 --- a/apps/webapp/app/v3/marqs/index.server.ts +++ b/apps/webapp/app/v3/marqs/index.server.ts @@ -325,8 +325,8 @@ export class MarQS { typeof timestamp === "undefined" ? Date.now() : typeof timestamp === "number" - ? timestamp - : timestamp.getTime(); + ? timestamp + : timestamp.getTime(); const messagePayload: MessagePayload = { version: "1", diff --git a/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts b/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts index 518b64666d..19d50faac3 100644 --- a/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts +++ b/apps/webapp/app/v3/marqs/sharedQueueConsumer.server.ts @@ -620,11 +620,11 @@ export class SharedQueueConsumer { return existingTaskRun.lockedById ? await getWorkerDeploymentFromWorkerTask(existingTaskRun.lockedById) : existingTaskRun.lockedToVersionId - ? await getWorkerDeploymentFromWorker(existingTaskRun.lockedToVersionId) - : await findCurrentWorkerDeployment({ - environmentId: existingTaskRun.runtimeEnvironmentId, - type: "V1", - }); + ? await getWorkerDeploymentFromWorker(existingTaskRun.lockedToVersionId) + : await findCurrentWorkerDeployment({ + environmentId: existingTaskRun.runtimeEnvironmentId, + type: "V1", + }); }); const worker = deployment?.worker; diff --git a/apps/webapp/app/v3/mollifier/applyMetadataMutation.server.ts b/apps/webapp/app/v3/mollifier/applyMetadataMutation.server.ts index bef87afa37..25fe6d73d6 100644 --- a/apps/webapp/app/v3/mollifier/applyMetadataMutation.server.ts +++ b/apps/webapp/app/v3/mollifier/applyMetadataMutation.server.ts @@ -81,10 +81,7 @@ export async function applyMetadataMutationToBufferedRun(input: { if (!entry) return { kind: "not_found" }; // Env+org check: an entry from a different env is treated as a // miss (not 403) so existence in other envs doesn't leak. - if ( - entry.envId !== input.environmentId || - entry.orgId !== input.organizationId - ) { + if (entry.envId !== input.environmentId || entry.orgId !== input.organizationId) { return { kind: "not_found" }; } if (entry.status !== "QUEUED" || entry.materialised) { @@ -146,7 +143,7 @@ export async function applyMetadataMutationToBufferedRun(input: { // metadata; ignore `body.metadata` to match PG behaviour. metadataObject = applyMetadataOperations( parseSnapshotMetadata(), - input.body.operations, + input.body.operations ).newMetadata; } else if (input.body.metadata !== undefined) { // No operations β€” full replace. diff --git a/apps/webapp/app/v3/mollifier/mollifierDrainer.server.ts b/apps/webapp/app/v3/mollifier/mollifierDrainer.server.ts index c520847ce3..dacefaac71 100644 --- a/apps/webapp/app/v3/mollifier/mollifierDrainer.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierDrainer.server.ts @@ -41,7 +41,7 @@ function initializeMollifierDrainer(): MollifierDrainer { // to nothing). Crashing surfaces the misconfig immediately rather // than silently leaving entries un-drained. throw new MollifierConfigurationError( - "MollifierDrainer initialised without a buffer β€” env vars inconsistent", + "MollifierDrainer initialised without a buffer β€” env vars inconsistent" ); } @@ -65,7 +65,7 @@ function initializeMollifierDrainer(): MollifierDrainer { env.GRACEFUL_SHUTDOWN_TIMEOUT - shutdownMarginMs ) { throw new MollifierConfigurationError( - `TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS (${env.TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS}) must be at least ${shutdownMarginMs}ms below GRACEFUL_SHUTDOWN_TIMEOUT (${env.GRACEFUL_SHUTDOWN_TIMEOUT}); otherwise the primary's hard exit shadows the drainer's deadline.`, + `TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS (${env.TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS}) must be at least ${shutdownMarginMs}ms below GRACEFUL_SHUTDOWN_TIMEOUT (${env.GRACEFUL_SHUTDOWN_TIMEOUT}); otherwise the primary's hard exit shadows the drainer's deadline.` ); } diff --git a/apps/webapp/app/v3/mollifier/mollifierDrainerHandler.server.ts b/apps/webapp/app/v3/mollifier/mollifierDrainerHandler.server.ts index 6e829baa57..fd9daa5d61 100644 --- a/apps/webapp/app/v3/mollifier/mollifierDrainerHandler.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierDrainerHandler.server.ts @@ -89,7 +89,7 @@ export function createDrainerHandler(deps: { cancelReason, emitRunCancelledEvent: false, }, - deps.prisma, + deps.prisma ); } catch (err) { // createCancelledRun throws a conflict when the normal trigger @@ -118,8 +118,10 @@ export function createDrainerHandler(deps: { if (isRetryablePgError(err)) { throw err; } - span.setAttribute("mollifier.cancel_terminal_failure_reason", - err instanceof Error ? err.message : String(err)); + span.setAttribute( + "mollifier.cancel_terminal_failure_reason", + err instanceof Error ? err.message : String(err) + ); try { const wrote = await writeMollifierTerminalFailureRow(deps, { friendlyId: input.runId, @@ -141,9 +143,7 @@ export function createDrainerHandler(deps: { } span.setAttribute("mollifier.cancel_conflict", true); const friendlyId = - typeof input.payload.friendlyId === "string" - ? input.payload.friendlyId - : input.runId; + typeof input.payload.friendlyId === "string" ? input.payload.friendlyId : input.runId; await deps.engine.cancelRun({ runId: RunId.fromFriendlyId(friendlyId), completedAt: new Date(cancelledAtStr), @@ -295,7 +295,7 @@ export function createDrainerHandler(deps: { // logic can own the decision. async function writeMollifierTerminalFailureRow( deps: { engine: RunEngine; prisma: PrismaClientOrTransaction }, - args: { friendlyId: string; snapshot: Record; reason: string }, + args: { friendlyId: string; snapshot: Record; reason: string } ) { const { snapshot } = args; const env = snapshot.environment as @@ -334,8 +334,7 @@ async function writeMollifierTerminalFailureRow( }, parentTaskRunId: typeof snapshot.parentTaskRunId === "string" ? snapshot.parentTaskRunId : undefined, - rootTaskRunId: - typeof snapshot.rootTaskRunId === "string" ? snapshot.rootTaskRunId : undefined, + rootTaskRunId: typeof snapshot.rootTaskRunId === "string" ? snapshot.rootTaskRunId : undefined, depth: typeof snapshot.depth === "number" ? snapshot.depth : 0, resumeParentOnCompletion: snapshot.resumeParentOnCompletion === true, batch, @@ -344,8 +343,7 @@ async function writeMollifierTerminalFailureRow( taskEventStore: typeof snapshot.taskEventStore === "string" ? snapshot.taskEventStore : undefined, queue: typeof snapshot.queue === "string" ? snapshot.queue : undefined, - lockedQueueId: - typeof snapshot.lockedQueueId === "string" ? snapshot.lockedQueueId : undefined, + lockedQueueId: typeof snapshot.lockedQueueId === "string" ? snapshot.lockedQueueId : undefined, emitRunFailedEvent: false, }); // Alerts side of `runFailed` β€” the engine emit was suppressed above diff --git a/apps/webapp/app/v3/mollifier/mollifierDrainingGauge.server.ts b/apps/webapp/app/v3/mollifier/mollifierDrainingGauge.server.ts index eda8f45ebf..85a2acd65c 100644 --- a/apps/webapp/app/v3/mollifier/mollifierDrainingGauge.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierDrainingGauge.server.ts @@ -19,10 +19,12 @@ let intervalHandle: ReturnType | null = null; // // Idempotent: a second call is a no-op (Remix dev hot-reload re-runs // the bootstrap; the existing interval keeps ticking). -export function startMollifierDrainingGauge(opts: { - intervalMs?: number; - getBuffer?: typeof getMollifierBuffer; -} = {}): void { +export function startMollifierDrainingGauge( + opts: { + intervalMs?: number; + getBuffer?: typeof getMollifierBuffer; + } = {} +): void { if (intervalHandle !== null) return; const intervalMs = opts.intervalMs ?? POLL_INTERVAL_MS; diff --git a/apps/webapp/app/v3/mollifier/mollifierGate.server.ts b/apps/webapp/app/v3/mollifier/mollifierGate.server.ts index 461faa8d3b..8e237b8e76 100644 --- a/apps/webapp/app/v3/mollifier/mollifierGate.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierGate.server.ts @@ -73,14 +73,8 @@ export type GateDependencies = { isShadowModeOn: () => boolean; resolveOrgFlag: (inputs: GateInputs) => Promise; evaluator: TripEvaluator; - logShadow: ( - inputs: GateInputs, - decision: Extract, - ) => void; - logMollified: ( - inputs: GateInputs, - decision: Extract, - ) => void; + logShadow: (inputs: GateInputs, decision: Extract) => void; + logMollified: (inputs: GateInputs, decision: Extract) => void; recordDecision: (outcome: DecisionOutcome, opts: RecordDecisionOptions) => void; }; @@ -99,7 +93,7 @@ const defaultEvaluator = createRealTripEvaluator({ function logDivertDecision( message: "mollifier.would_mollify" | "mollifier.mollified", inputs: GateInputs, - decision: Extract, + decision: Extract ): void { logger.debug(message, { envId: inputs.envId, @@ -140,16 +134,14 @@ export const defaultGateDependencies: GateDependencies = { isShadowModeOn: () => env.TRIGGER_MOLLIFIER_SHADOW_MODE === "1", resolveOrgFlag: resolveMollifierFlag, evaluator: defaultEvaluator, - logShadow: (inputs, decision) => - logDivertDecision("mollifier.would_mollify", inputs, decision), - logMollified: (inputs, decision) => - logDivertDecision("mollifier.mollified", inputs, decision), + logShadow: (inputs, decision) => logDivertDecision("mollifier.would_mollify", inputs, decision), + logMollified: (inputs, decision) => logDivertDecision("mollifier.mollified", inputs, decision), recordDecision, }; export async function evaluateGate( inputs: GateInputs, - deps: Partial = {}, + deps: Partial = {} ): Promise { const d = { ...defaultGateDependencies, ...deps }; diff --git a/apps/webapp/app/v3/mollifier/mollifierMollify.server.ts b/apps/webapp/app/v3/mollifier/mollifierMollify.server.ts index a8b0b15111..6ebcf4a248 100644 --- a/apps/webapp/app/v3/mollifier/mollifierMollify.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierMollify.server.ts @@ -35,8 +35,7 @@ export type MollifySyntheticResult = { const NOTICE: MollifyNotice = { code: "mollifier.queued", - message: - "Trigger accepted into burst buffer. Consider batchTrigger for fan-outs of 100+.", + message: "Trigger accepted into burst buffer. Consider batchTrigger for fan-outs of 100+.", docs: "https://trigger.dev/docs/management/tasks/batch-trigger", }; diff --git a/apps/webapp/app/v3/mollifier/mollifierStaleSweep.server.ts b/apps/webapp/app/v3/mollifier/mollifierStaleSweep.server.ts index d135824032..c2c2a2d03d 100644 --- a/apps/webapp/app/v3/mollifier/mollifierStaleSweep.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierStaleSweep.server.ts @@ -1,7 +1,10 @@ import type { MollifierBuffer } from "@trigger.dev/redis-worker"; import { logger as defaultLogger } from "~/services/logger.server"; import { getMollifierBuffer } from "./mollifierBuffer.server"; -import { MollifierStaleSweepState, type StaleSweepStateStore } from "./mollifierStaleSweepState.server"; +import { + MollifierStaleSweepState, + type StaleSweepStateStore, +} from "./mollifierStaleSweepState.server"; import { recordStaleEntry as defaultRecordStaleEntry, reportStaleEntrySnapshot as defaultReportStaleEntrySnapshot, @@ -81,12 +84,11 @@ export type StaleSweepResult = { // every tick) does not bound work. export async function runStaleSweepOnce( config: StaleSweepConfig, - deps: StaleSweepDeps, + deps: StaleSweepDeps ): Promise { const getBuffer = deps.getBuffer ?? getMollifierBuffer; const recordStale = deps.recordStaleEntry ?? defaultRecordStaleEntry; - const reportSnapshot = - deps.reportStaleEntrySnapshot ?? defaultReportStaleEntrySnapshot; + const reportSnapshot = deps.reportStaleEntrySnapshot ?? defaultReportStaleEntrySnapshot; const log = deps.logger ?? defaultLogger; const now = (deps.now ?? Date.now)(); const maxEntries = config.maxEntriesPerEnv ?? DEFAULT_MAX_ENTRIES_PER_ENV; @@ -114,10 +116,7 @@ export async function runStaleSweepOnce( await deps.state.rebuildOrgList(orgs); } - const { orgs: slice, total } = await deps.state.readOrgListSlice( - cursor, - maxOrgsPerPass, - ); + const { orgs: slice, total } = await deps.state.readOrgListSlice(cursor, maxOrgsPerPass); let envsScanned = 0; let entriesScanned = 0; @@ -194,7 +193,7 @@ export type StaleSweepIntervalHandle = { // overlapping sweeps that all log the same stale entries). export function startStaleSweepInterval( config: StaleSweepConfig & { intervalMs: number }, - deps: StaleSweepDeps, + deps: StaleSweepDeps ): StaleSweepIntervalHandle { let stopped = false; let inFlight = false; diff --git a/apps/webapp/app/v3/mollifier/mollifierStaleSweepState.server.ts b/apps/webapp/app/v3/mollifier/mollifierStaleSweepState.server.ts index a6f3ff3e4a..9be3c171ce 100644 --- a/apps/webapp/app/v3/mollifier/mollifierStaleSweepState.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierStaleSweepState.server.ts @@ -70,7 +70,7 @@ export class MollifierStaleSweepState implements StaleSweepStateStore { onError: (error) => { this.logger.error("MollifierStaleSweepState redis client error:", { error }); }, - }, + } ); } @@ -97,10 +97,7 @@ export class MollifierStaleSweepState implements StaleSweepStateStore { await pipeline.exec(); } - async readOrgListSlice( - start: number, - count: number, - ): Promise<{ orgs: string[]; total: number }> { + async readOrgListSlice(start: number, count: number): Promise<{ orgs: string[]; total: number }> { const pipeline = this.redis.pipeline(); pipeline.lrange(ORG_LIST_KEY, start, start + count - 1); pipeline.llen(ORG_LIST_KEY); diff --git a/apps/webapp/app/v3/mollifier/mollifierTelemetry.server.ts b/apps/webapp/app/v3/mollifier/mollifierTelemetry.server.ts index 4bd2c45eb5..6310ad9d51 100644 --- a/apps/webapp/app/v3/mollifier/mollifierTelemetry.server.ts +++ b/apps/webapp/app/v3/mollifier/mollifierTelemetry.server.ts @@ -30,7 +30,7 @@ export type RecordDecisionOptions = { // unit-testable without standing up an OTel meter. export function decisionLabels( outcome: DecisionOutcome, - opts: RecordDecisionOptions, + opts: RecordDecisionOptions ): Record { return { outcome, @@ -54,7 +54,7 @@ export const realtimeBufferedSubscriptionsCounter = meter.createCounter( { description: "Realtime subscriptions opened against a runId that exists only in the mollifier buffer", - }, + } ); // No `envId` attribute β€” `envId` is a banned high-cardinality metric @@ -72,13 +72,9 @@ 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", - { - description: - "Mollifier buffer entries whose dwell exceeds the stale threshold (per sweep pass)", - }, -); +export const staleEntriesCounter = meter.createCounter("mollifier.stale_entries", { + description: "Mollifier buffer entries whose dwell exceeds the stale threshold (per sweep pass)", +}); // No `envId` attribute β€” see comment above. export function recordStaleEntry(): void { @@ -90,13 +86,10 @@ 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", - { - description: - "Buffer entries whose dwell exceeds the stale threshold, as observed by the latest sweep pass", - }, -); +export const staleEntriesGauge = meter.createObservableGauge("mollifier.stale_entries.current", { + description: + "Buffer entries whose dwell exceeds the stale threshold, as observed by the latest sweep pass", +}); let latestStaleTotal = 0; @@ -115,7 +108,7 @@ meter.addBatchObservableCallback( (result) => { result.observe(staleEntriesGauge, latestStaleTotal); }, - [staleEntriesGauge], + [staleEntriesGauge] ); // Observability gauge for entries currently in DRAINING state β€” popped @@ -130,13 +123,10 @@ 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", - { - description: - "Mollifier buffer entries currently in DRAINING state (popped but not yet acked/failed/requeued)", - }, -); +export const drainingCountGauge = meter.createObservableGauge("mollifier.draining.current", { + description: + "Mollifier buffer entries currently in DRAINING state (popped but not yet acked/failed/requeued)", +}); let latestDrainingCount = 0; @@ -148,7 +138,7 @@ meter.addBatchObservableCallback( (result) => { result.observe(drainingCountGauge, latestDrainingCount); }, - [drainingCountGauge], + [drainingCountGauge] ); // Electric SQL's shape-stream protocol adds a `handle=` query param on diff --git a/apps/webapp/app/v3/mollifier/mutateWithFallback.server.ts b/apps/webapp/app/v3/mollifier/mutateWithFallback.server.ts index 6ed2d9a1e0..8460fbe541 100644 --- a/apps/webapp/app/v3/mollifier/mutateWithFallback.server.ts +++ b/apps/webapp/app/v3/mollifier/mutateWithFallback.server.ts @@ -37,18 +37,14 @@ export type MutateWithFallbackInput = { // matching the PG path, without an extra Redis round-trip. // `bufferEntry` is `null` in the rare race where the entry didn't // exist at pre-check time but appeared before `mutateSnapshot`. - synthesisedResponse: (ctx: { - bufferEntry: BufferEntry | null; - }) => TResponse | Promise; + synthesisedResponse: (ctx: { bufferEntry: BufferEntry | null }) => TResponse | Promise; // Called when the buffer rejected the patch as invalid (e.g. an // `append_tags` patch carrying `maxTags` would exceed the cap). Required // only by callers that send a rejectable patch; the helper throws if the // buffer reports a rejection and no builder was supplied. Receives the // same `bufferEntry` context as `synthesisedResponse` so a rejection // message can reference the prior state if useful. - rejectedResponse?: (ctx: { - bufferEntry: BufferEntry | null; - }) => TResponse | Promise; + rejectedResponse?: (ctx: { bufferEntry: BufferEntry | null }) => TResponse | Promise; abortSignal?: AbortSignal; // Override defaults for tests. safetyNetMs?: number; @@ -78,7 +74,7 @@ export type MutateWithFallbackOutcome = // this helper never throws Response objects so it remains route-agnostic // and unit-testable in isolation. export async function mutateWithFallback( - input: MutateWithFallbackInput, + input: MutateWithFallbackInput ): Promise> { const replica = input.prismaReplica ?? $replica; const writer = input.prismaWriter ?? prisma; @@ -122,8 +118,7 @@ export async function mutateWithFallback( const entryForAuth = await buffer.getEntry(input.runId); if ( entryForAuth && - (entryForAuth.envId !== input.environmentId || - entryForAuth.orgId !== input.organizationId) + (entryForAuth.envId !== input.environmentId || entryForAuth.orgId !== input.organizationId) ) { // Hide existence on env mismatch: return not_found, same shape as // a true miss, rather than 403 which would leak that the runId @@ -132,10 +127,7 @@ export async function mutateWithFallback( } // Path 2 β€” buffer snapshot mutation. - const result: MutateSnapshotResult = await buffer.mutateSnapshot( - input.runId, - input.bufferPatch, - ); + const result: MutateSnapshotResult = await buffer.mutateSnapshot(input.runId, input.bufferPatch); if (result === "applied_to_snapshot") { return { @@ -150,7 +142,7 @@ export async function mutateWithFallback( // caller sent a rejectable patch without handling the rejection. if (!input.rejectedResponse) { throw new Error( - "mutateWithFallback: buffer returned 'limit_exceeded' but no rejectedResponse was provided", + "mutateWithFallback: buffer returned 'limit_exceeded' but no rejectedResponse was provided" ); } return { @@ -227,7 +219,7 @@ export async function mutateWithFallback( async function findRunInPg( client: PrismaClientOrTransaction | PrismaReplicaClient, friendlyId: string, - environmentId: string, + environmentId: string ): Promise { return runStore.findRun({ friendlyId, runtimeEnvironmentId: environmentId }, client); } diff --git a/apps/webapp/app/v3/mollifier/readFallback.server.ts b/apps/webapp/app/v3/mollifier/readFallback.server.ts index d684741219..188face643 100644 --- a/apps/webapp/app/v3/mollifier/readFallback.server.ts +++ b/apps/webapp/app/v3/mollifier/readFallback.server.ts @@ -116,7 +116,9 @@ function asString(value: unknown): string | undefined { } function asStringArray(value: unknown): string[] { - return Array.isArray(value) && value.every((v) => typeof v === "string") ? (value as string[]) : []; + return Array.isArray(value) && value.every((v) => typeof v === "string") + ? (value as string[]) + : []; } function asDate(value: unknown): Date | undefined { @@ -137,7 +139,7 @@ function internalRunIdToFriendlyId(internalId: string | undefined): string | und export async function findRunByIdWithMollifierFallback( input: ReadFallbackInput, - deps: ReadFallbackDeps = {}, + deps: ReadFallbackDeps = {} ): Promise { const buffer = (deps.getBuffer ?? getMollifierBuffer)(); if (!buffer) return null; @@ -164,7 +166,7 @@ export async function findRunByIdWithMollifierFallback( // instead of the customer-supplied key β€” diverging from how // materialised runs render the same field. const idempotencyKeyOptionsParsed = IdempotencyKeyOptionsSchema.safeParse( - snapshot.idempotencyKeyOptions, + snapshot.idempotencyKeyOptions ); const idempotencyKeyOptions = idempotencyKeyOptionsParsed.success ? idempotencyKeyOptionsParsed.data @@ -219,8 +221,7 @@ export async function findRunByIdWithMollifierFallback( spanId: asString(snapshot.spanId), parentSpanId: asString(snapshot.parentSpanId), - runtimeEnvironmentId: - asString(environment?.id) ?? entry.envId, + runtimeEnvironmentId: asString(environment?.id) ?? entry.envId, engine: "V2", workerQueue: asString(snapshot.workerQueue), region: asString(snapshot.region), @@ -249,12 +250,8 @@ export async function findRunByIdWithMollifierFallback( // not the friendlyIds the SyntheticRun contract expects. Convert // internal β†’ friendly here so consumers don't have to special-case // the buffered path. - parentTaskRunFriendlyId: internalRunIdToFriendlyId( - asString(snapshot.parentTaskRunId) - ), - rootTaskRunFriendlyId: internalRunIdToFriendlyId( - asString(snapshot.rootTaskRunId) - ), + parentTaskRunFriendlyId: internalRunIdToFriendlyId(asString(snapshot.parentTaskRunId)), + rootTaskRunFriendlyId: internalRunIdToFriendlyId(asString(snapshot.rootTaskRunId)), error: entry.lastError, }; diff --git a/apps/webapp/app/v3/mollifier/resolveRunForMutation.server.ts b/apps/webapp/app/v3/mollifier/resolveRunForMutation.server.ts index fc251469c8..258c0da211 100644 --- a/apps/webapp/app/v3/mollifier/resolveRunForMutation.server.ts +++ b/apps/webapp/app/v3/mollifier/resolveRunForMutation.server.ts @@ -45,11 +45,7 @@ export async function resolveRunForMutation(input: { if (buffer) { const entry = await buffer.getEntry(input.runParam); - if ( - entry && - entry.envId === input.environmentId && - entry.orgId === input.organizationId - ) { + if (entry && entry.envId === input.environmentId && entry.orgId === input.organizationId) { return { source: "buffer", friendlyId: input.runParam }; } } diff --git a/apps/webapp/app/v3/mollifier/syntheticRedirectInfo.server.ts b/apps/webapp/app/v3/mollifier/syntheticRedirectInfo.server.ts index e316846d70..4bc6b9f9be 100644 --- a/apps/webapp/app/v3/mollifier/syntheticRedirectInfo.server.ts +++ b/apps/webapp/app/v3/mollifier/syntheticRedirectInfo.server.ts @@ -57,7 +57,7 @@ export async function findBufferedRunRedirectInfo( // org membership in the PG query either). skipOrgMembershipCheck?: boolean; }, - deps: FindBufferedRunRedirectInfoDeps = {}, + deps: FindBufferedRunRedirectInfoDeps = {} ): Promise { const buffer = (deps.getBuffer ?? getMollifierBuffer)(); const prismaClient = deps.prismaClient ?? prisma; diff --git a/apps/webapp/app/v3/mollifier/syntheticRunHeader.server.ts b/apps/webapp/app/v3/mollifier/syntheticRunHeader.server.ts index 9b137f87fb..4b505fcb57 100644 --- a/apps/webapp/app/v3/mollifier/syntheticRunHeader.server.ts +++ b/apps/webapp/app/v3/mollifier/syntheticRunHeader.server.ts @@ -46,8 +46,8 @@ export function buildSyntheticRunHeader(args: { status: isCancelled ? ("CANCELED" as const) : isFailed - ? ("SYSTEM_FAILURE" as const) - : ("PENDING" as const), + ? ("SYSTEM_FAILURE" as const) + : ("PENDING" as const), isFinished: isCancelled || isFailed, startedAt: null, // Symmetric with `buildSyntheticSpanRun` and the diff --git a/apps/webapp/app/v3/mollifier/syntheticSpanRun.server.ts b/apps/webapp/app/v3/mollifier/syntheticSpanRun.server.ts index ae274aac3d..d5d682426d 100644 --- a/apps/webapp/app/v3/mollifier/syntheticSpanRun.server.ts +++ b/apps/webapp/app/v3/mollifier/syntheticSpanRun.server.ts @@ -32,7 +32,11 @@ function narrowMachinePreset(value: string | undefined): SpanRun["machinePreset" // snapshot fields as inline packets. export async function buildSyntheticSpanRun(args: { run: SyntheticRun; - environment: { id: string; slug: string; type: "PRODUCTION" | "DEVELOPMENT" | "STAGING" | "PREVIEW" }; + environment: { + id: string; + slug: string; + type: "PRODUCTION" | "DEVELOPMENT" | "STAGING" | "PREVIEW"; + }; }): Promise { const { run, environment } = args; @@ -63,8 +67,8 @@ export async function buildSyntheticSpanRun(args: { const idempotencyKeyStatus: SpanRun["idempotencyKeyStatus"] = idempotencyKey ? "active" : idempotencyKeyScope - ? "inactive" - : undefined; + ? "inactive" + : undefined; const taskKind = RunAnnotations.safeParse(run.annotations).data?.taskKind; const isAgentRun = taskKind === "AGENT"; @@ -82,8 +86,8 @@ export async function buildSyntheticSpanRun(args: { const status: SpanRun["status"] = isCancelled ? "CANCELED" : isFailed - ? "SYSTEM_FAILURE" - : "PENDING"; + ? "SYSTEM_FAILURE" + : "PENDING"; // Mirror ApiRetrieveRunPresenter's STRING_ERROR synthesis so the panel // shows why a buffered run failed instead of an empty error block. @@ -97,10 +101,10 @@ export async function buildSyntheticSpanRun(args: { friendlyId: run.friendlyId, status, statusReason: isCancelled - ? run.cancelReason ?? undefined + ? (run.cancelReason ?? undefined) : isFailed - ? run.error?.message ?? undefined - : undefined, + ? (run.error?.message ?? undefined) + : undefined, createdAt: run.createdAt, startedAt: null, executedAt: null, @@ -177,7 +181,7 @@ export async function buildSyntheticSpanRun(args: { }, }, null, - 2, + 2 ), metadata, maxDurationInSeconds: getMaxDuration(run.maxDurationInSeconds), diff --git a/apps/webapp/app/v3/mollifier/syntheticTrace.server.ts b/apps/webapp/app/v3/mollifier/syntheticTrace.server.ts index 03b6e03ca1..e36aaa19e6 100644 --- a/apps/webapp/app/v3/mollifier/syntheticTrace.server.ts +++ b/apps/webapp/app/v3/mollifier/syntheticTrace.server.ts @@ -41,9 +41,7 @@ export function buildSyntheticTraceForBufferedRun(run: SyntheticRun) { const events = tree ? flattenTree(tree).map((n) => { - const offset = millisecondsToNanoseconds( - n.data.startTime.getTime() - treeRootStartTimeMs - ); + const offset = millisecondsToNanoseconds(n.data.startTime.getTime() - treeRootStartTimeMs); // Mirror RunPresenter: raw span events stay server-side, only // timelineEvents ship to the client. const { events: spanEvents, ...data } = n.data; @@ -51,7 +49,11 @@ export function buildSyntheticTraceForBufferedRun(run: SyntheticRun) { ...n, data: { ...data, - timelineEvents: createTimelineSpanEventsFromSpanEvents(spanEvents, false, treeRootStartTimeMs), + timelineEvents: createTimelineSpanEventsFromSpanEvents( + spanEvents, + false, + treeRootStartTimeMs + ), duration: n.data.isPartial ? null : n.data.duration, offset, isRoot: n.id === spanId, diff --git a/apps/webapp/app/v3/mollifierDrainerWorker.server.ts b/apps/webapp/app/v3/mollifierDrainerWorker.server.ts index b79b9f08e5..1ffa48f1cd 100644 --- a/apps/webapp/app/v3/mollifierDrainerWorker.server.ts +++ b/apps/webapp/app/v3/mollifierDrainerWorker.server.ts @@ -52,7 +52,7 @@ export function initMollifierDrainerWorker( // without manipulating module-level env state. isEnabled?: () => boolean; getDrainer?: typeof getMollifierDrainer; - } = {}, + } = {} ): void { const isEnabled = opts.isEnabled ?? (() => env.TRIGGER_MOLLIFIER_DRAINER_ENABLED === "1"); const getDrainer = opts.getDrainer ?? getMollifierDrainer; diff --git a/apps/webapp/app/v3/mollifierStaleSweepWorker.server.ts b/apps/webapp/app/v3/mollifierStaleSweepWorker.server.ts index e147422bfe..2247a6bdff 100644 --- a/apps/webapp/app/v3/mollifierStaleSweepWorker.server.ts +++ b/apps/webapp/app/v3/mollifierStaleSweepWorker.server.ts @@ -60,7 +60,7 @@ export function initMollifierStaleSweepWorker(): void { maxEntriesPerEnv: env.TRIGGER_MOLLIFIER_STALE_SWEEP_MAX_ENTRIES_PER_ENV, maxOrgsPerPass: env.TRIGGER_MOLLIFIER_STALE_SWEEP_MAX_ORGS_PER_PASS, }, - { state }, + { state } ); // `handle.stop` is now async (it closes the Redis client). The signals diff --git a/apps/webapp/app/v3/objectStore.server.ts b/apps/webapp/app/v3/objectStore.server.ts index 16aa9d71ee..159f2f41c6 100644 --- a/apps/webapp/app/v3/objectStore.server.ts +++ b/apps/webapp/app/v3/objectStore.server.ts @@ -283,11 +283,7 @@ export async function downloadPacketFromObjectStore( } const { protocol, path } = parseStorageUri(packet.data); - const key = buildPacketObjectStoreKey( - environment.project.externalRef, - environment.slug, - path - ); + const key = buildPacketObjectStoreKey(environment.project.externalRef, environment.slug, path); const client = getObjectStoreClient(protocol); diff --git a/apps/webapp/app/v3/objectStoreClient.server.ts b/apps/webapp/app/v3/objectStoreClient.server.ts index 9999a40b79..790530de35 100644 --- a/apps/webapp/app/v3/objectStoreClient.server.ts +++ b/apps/webapp/app/v3/objectStoreClient.server.ts @@ -46,7 +46,11 @@ class Aws4FetchClient implements IObjectStoreClient { return url.toString(); } - async putObject(key: string, body: ReadableStream | string, contentType: string): Promise { + async putObject( + key: string, + body: ReadableStream | string, + contentType: string + ): Promise { const objectUrl = this.buildUrl(key); const response = await this.awsClient.fetch(objectUrl, { method: "PUT", @@ -114,7 +118,11 @@ class AwsSdkClient implements IObjectStoreClient { return url.href; } - async putObject(key: string, body: ReadableStream | string, contentType: string): Promise { + async putObject( + key: string, + body: ReadableStream | string, + contentType: string + ): Promise { const s3Key = this.toS3ObjectKey(key); await this.s3Client.send( new PutObjectCommand({ diff --git a/apps/webapp/app/v3/otlpExporter.server.ts b/apps/webapp/app/v3/otlpExporter.server.ts index 975e4aed4a..148109a034 100644 --- a/apps/webapp/app/v3/otlpExporter.server.ts +++ b/apps/webapp/app/v3/otlpExporter.server.ts @@ -745,16 +745,16 @@ function convertKeyValueItemsToMap( map[`${prefix ? `${prefix}.` : ""}${attribute.key}`] = isStringValue(attribute.value) ? attribute.value.stringValue : isIntValue(attribute.value) - ? Number(attribute.value.intValue) - : isDoubleValue(attribute.value) - ? attribute.value.doubleValue - : isBoolValue(attribute.value) - ? attribute.value.boolValue - : isBytesValue(attribute.value) - ? binaryToHex(attribute.value.bytesValue) - : isArrayValue(attribute.value) - ? serializeArrayValue(attribute.value.arrayValue!.values) - : undefined; + ? Number(attribute.value.intValue) + : isDoubleValue(attribute.value) + ? attribute.value.doubleValue + : isBoolValue(attribute.value) + ? attribute.value.boolValue + : isBytesValue(attribute.value) + ? binaryToHex(attribute.value.bytesValue) + : isArrayValue(attribute.value) + ? serializeArrayValue(attribute.value.arrayValue!.values) + : undefined; return map; }, @@ -783,16 +783,16 @@ function convertSelectedKeyValueItemsToMap( map[`${prefix ? `${prefix}.` : ""}${attribute.key}`] = isStringValue(attribute.value) ? attribute.value.stringValue : isIntValue(attribute.value) - ? Number(attribute.value.intValue) - : isDoubleValue(attribute.value) - ? attribute.value.doubleValue - : isBoolValue(attribute.value) - ? attribute.value.boolValue - : isBytesValue(attribute.value) - ? binaryToHex(attribute.value.bytesValue) - : isArrayValue(attribute.value) - ? serializeArrayValue(attribute.value.arrayValue!.values) - : undefined; + ? Number(attribute.value.intValue) + : isDoubleValue(attribute.value) + ? attribute.value.doubleValue + : isBoolValue(attribute.value) + ? attribute.value.boolValue + : isBytesValue(attribute.value) + ? binaryToHex(attribute.value.bytesValue) + : isArrayValue(attribute.value) + ? serializeArrayValue(attribute.value.arrayValue!.values) + : undefined; return map; }, diff --git a/apps/webapp/app/v3/querySchemas.ts b/apps/webapp/app/v3/querySchemas.ts index 304664800e..4784ad7562 100644 --- a/apps/webapp/app/v3/querySchemas.ts +++ b/apps/webapp/app/v3/querySchemas.ts @@ -198,7 +198,8 @@ export const runsSchema: TableSchema = { example: "us-east-1", }), // No whereTransform: the expression drives WHERE too, so pre-region rows still match. - expression: "multiIf(region != '', region, startsWith(worker_queue, 'cm'), NULL, worker_queue)", + expression: + "multiIf(region != '', region, startsWith(worker_queue, 'cm'), NULL, worker_queue)", }, // Timing diff --git a/apps/webapp/app/v3/runEngine.server.ts b/apps/webapp/app/v3/runEngine.server.ts index 06cb591a0b..3f9cd603b0 100644 --- a/apps/webapp/app/v3/runEngine.server.ts +++ b/apps/webapp/app/v3/runEngine.server.ts @@ -21,8 +21,7 @@ function createRunEngine() { logLevel: env.RUN_ENGINE_WORKER_LOG_LEVEL, treatProductionExecutionStallsAsOOM: env.RUN_ENGINE_TREAT_PRODUCTION_EXECUTION_STALLS_AS_OOM === "1", - readReplicaSnapshotsSinceEnabled: - env.RUN_ENGINE_READ_REPLICA_SNAPSHOTS_SINCE_ENABLED === "1", + readReplicaSnapshotsSinceEnabled: env.RUN_ENGINE_READ_REPLICA_SNAPSHOTS_SINCE_ENABLED === "1", readReplicaSnapshotsSinceRetryDelay: { minMs: env.RUN_ENGINE_SNAPSHOTS_SINCE_REPLICA_RETRY_MIN_MS, maxMs: env.RUN_ENGINE_SNAPSHOTS_SINCE_REPLICA_RETRY_MAX_MS, diff --git a/apps/webapp/app/v3/runEngineHandlers.server.ts b/apps/webapp/app/v3/runEngineHandlers.server.ts index e2285a4fec..a9c9df5837 100644 --- a/apps/webapp/app/v3/runEngineHandlers.server.ts +++ b/apps/webapp/app/v3/runEngineHandlers.server.ts @@ -896,7 +896,7 @@ export function setupBatchQueueCallbacks() { batchIndex: itemIndex, realtimeStreamsVersion: meta.realtimeStreamsVersion, planType: meta.planType, - triggerSource: meta.parentRunId ? "sdk" : meta.triggerSource ?? "api", + triggerSource: meta.parentRunId ? "sdk" : (meta.triggerSource ?? "api"), triggerAction: "trigger", }, "V2" diff --git a/apps/webapp/app/v3/services/adminWorker.server.ts b/apps/webapp/app/v3/services/adminWorker.server.ts index cf3dbd57c6..6b416b45b7 100644 --- a/apps/webapp/app/v3/services/adminWorker.server.ts +++ b/apps/webapp/app/v3/services/adminWorker.server.ts @@ -14,8 +14,7 @@ import { runsReplicationInstance } from "~/services/runsReplicationInstance.serv // initializer never fires. Assignment to globalThis is an observable side // effect the bundler must preserve. See TRI-9864. import { sessionsReplicationInstance } from "~/services/sessionsReplicationInstance.server"; -(globalThis as Record).__sessionsReplicationInstance = - sessionsReplicationInstance; +(globalThis as Record).__sessionsReplicationInstance = sessionsReplicationInstance; import { singleton } from "~/utils/singleton"; import { tracer } from "../tracer.server"; import { $replica } from "~/db.server"; diff --git a/apps/webapp/app/v3/services/alerts/deliverAlert.server.ts b/apps/webapp/app/v3/services/alerts/deliverAlert.server.ts index 49f464d6dc..247efb5d48 100644 --- a/apps/webapp/app/v3/services/alerts/deliverAlert.server.ts +++ b/apps/webapp/app/v3/services/alerts/deliverAlert.server.ts @@ -33,10 +33,7 @@ import { ProjectAlertWebhookProperties, } from "~/models/projectAlert.server"; import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server"; -import { - processGitMetadata, - type GitMetaLinks, -} from "~/presenters/v3/BranchesPresenter.server"; +import { processGitMetadata, type GitMetaLinks } from "~/presenters/v3/BranchesPresenter.server"; import { DeploymentPresenter } from "~/presenters/v3/DeploymentPresenter.server"; import { sendAlertEmail } from "~/services/email.server"; import { VercelProjectIntegrationDataSchema } from "~/v3/vercel/vercelProjectIntegrationSchema"; @@ -164,9 +161,7 @@ export class DeliverAlertService extends BaseService { const deploymentMeta = alert.type === "DEPLOYMENT_SUCCESS" || alert.type === "DEPLOYMENT_FAILURE" - ? ( - await fromPromise(this.#resolveDeploymentMetadata(alert), (e) => e) - ).unwrapOr(emptyMeta) + ? (await fromPromise(this.#resolveDeploymentMetadata(alert), (e) => e)).unwrapOr(emptyMeta) : emptyMeta; try { @@ -774,7 +769,14 @@ export class DeliverAlertService extends BaseService { text: this.#wrapInCodeBlock(error.stackTrace ?? error.message), }, }, - this.#buildRunQuoteBlock(taskIdentifier, version, environment, runId, alert.project.name, timestamp), + this.#buildRunQuoteBlock( + taskIdentifier, + version, + environment, + runId, + alert.project.name, + timestamp + ), { type: "actions", elements: [ @@ -862,7 +864,13 @@ export class DeliverAlertService extends BaseService { text: this.#wrapInCodeBlock(preparedError.stack ?? preparedError.message), }, }, - this.#buildDeploymentQuoteBlock(alert, deploymentMeta, version, environment, timestamp), + this.#buildDeploymentQuoteBlock( + alert, + deploymentMeta, + version, + environment, + timestamp + ), { type: "actions", elements: [ @@ -903,7 +911,13 @@ export class DeliverAlertService extends BaseService { text: `:rocket: Deployed *${version}.${environment}* successfully`, }, }, - this.#buildDeploymentQuoteBlock(alert, deploymentMeta, version, environment, timestamp), + this.#buildDeploymentQuoteBlock( + alert, + deploymentMeta, + version, + environment, + timestamp + ), { type: "actions", elements: [ @@ -1057,9 +1071,7 @@ export class DeliverAlertService extends BaseService { } } - async #resolveDeploymentMetadata( - alert: FoundAlert - ): Promise { + async #resolveDeploymentMetadata(alert: FoundAlert): Promise { const deployment = alert.workerDeployment; if (!deployment) { return { git: null, vercelDeploymentUrl: undefined }; @@ -1078,20 +1090,19 @@ export class DeliverAlertService extends BaseService { projectId: string, deploymentId: string ): Promise { - const vercelProjectIntegration = - await this._prisma.organizationProjectIntegration.findFirst({ - where: { - projectId, + const vercelProjectIntegration = await this._prisma.organizationProjectIntegration.findFirst({ + where: { + projectId, + deletedAt: null, + organizationIntegration: { + service: "VERCEL", deletedAt: null, - organizationIntegration: { - service: "VERCEL", - deletedAt: null, - }, - }, - select: { - integrationData: true, }, - }); + }, + select: { + integrationData: true, + }, + }); if (!vercelProjectIntegration) { return undefined; @@ -1105,19 +1116,18 @@ export class DeliverAlertService extends BaseService { return undefined; } - const integrationDeployment = - await this._prisma.integrationDeployment.findFirst({ - where: { - deploymentId, - integrationName: "vercel", - }, - select: { - integrationDeploymentId: true, - }, - orderBy: { - createdAt: "desc", - }, - }); + const integrationDeployment = await this._prisma.integrationDeployment.findFirst({ + where: { + deploymentId, + integrationName: "vercel", + }, + select: { + integrationDeploymentId: true, + }, + orderBy: { + createdAt: "desc", + }, + }); if (!integrationDeployment) { return undefined; diff --git a/apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts b/apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts index 647a0b0cf3..b9c1e7ddf9 100644 --- a/apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts +++ b/apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts @@ -68,7 +68,11 @@ export class DeliverErrorGroupAlertService { return; } - const errorLink = this.#buildErrorLink(channel.project.organization, channel.project, payload.error); + const errorLink = this.#buildErrorLink( + channel.project.organization, + channel.project, + payload.error + ); try { switch (channel.type) { @@ -86,7 +90,9 @@ export class DeliverErrorGroupAlertService { } } catch (error) { if (error instanceof SkipRetryError) { - logger.warn("[DeliverErrorGroupAlert] Skipping retry", { reason: (error as Error).message }); + logger.warn("[DeliverErrorGroupAlert] Skipping retry", { + reason: (error as Error).message, + }); return; } throw error; @@ -113,7 +119,11 @@ export class DeliverErrorGroupAlertService { } async #sendEmail( - channel: { type: ProjectAlertChannelType; properties: unknown; project: { name: string; organization: { title: string } } }, + channel: { + type: ProjectAlertChannelType; + properties: unknown; + project: { name: string; organization: { title: string } }; + }, payload: ErrorAlertPayload, errorLink: string ): Promise { @@ -182,11 +192,7 @@ export class DeliverErrorGroupAlertService { return; } - const message = this.#buildErrorGroupSlackMessage( - payload, - errorLink, - channel.project.name - ); + const message = this.#buildErrorGroupSlackMessage(payload, errorLink, channel.project.name); await this.#postSlackMessage(integration, { channel: slackProperties.data.channelId, @@ -198,7 +204,14 @@ export class DeliverErrorGroupAlertService { channel: { type: ProjectAlertChannelType; properties: unknown; - project: { id: string; externalRef: string; slug: string; name: string; organizationId: string; organization: { slug: string; title: string } }; + project: { + id: string; + externalRef: string; + slug: string; + name: string; + organizationId: string; + organization: { slug: string; title: string }; + }; }, payload: ErrorAlertPayload, errorLink: string diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index c001932baa..a9d3a8f2f2 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -335,15 +335,18 @@ export class BatchTriggerV3Service extends BaseService { } // Group items by taskIdentifier - const itemsByTask = body.items.reduce((acc, item) => { - if (!item.options?.idempotencyKey) return acc; + const itemsByTask = body.items.reduce( + (acc, item) => { + if (!item.options?.idempotencyKey) return acc; - if (!acc[item.task]) { - acc[item.task] = []; - } - acc[item.task].push(item); - return acc; - }, {} as Record); + if (!acc[item.task]) { + acc[item.task] = []; + } + acc[item.task].push(item); + return acc; + }, + {} as Record + ); logger.debug("[BatchTriggerV2][call] Grouped items by task identifier", { itemsByTask, @@ -933,7 +936,12 @@ export class BatchTriggerV3Service extends BaseService { const filename = `${pathPrefix}/payload.json`; - const uploadedFilename = await uploadPacketToObjectStore(filename, packet.data, packet.dataType, environment); + const uploadedFilename = await uploadPacketToObjectStore( + filename, + packet.data, + packet.dataType, + environment + ); return { data: uploadedFilename, diff --git a/apps/webapp/app/v3/services/bulk/BulkActionV2.server.ts b/apps/webapp/app/v3/services/bulk/BulkActionV2.server.ts index 76d550c700..579f9a42b1 100644 --- a/apps/webapp/app/v3/services/bulk/BulkActionV2.server.ts +++ b/apps/webapp/app/v3/services/bulk/BulkActionV2.server.ts @@ -38,7 +38,10 @@ export class BulkActionService extends BaseService { const filters = await getFilters(payload, request); // Count the runs that will be affected by the bulk action - const clickhouse = await clickhouseFactory.getClickhouseForOrganization(organizationId, "standard"); + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + organizationId, + "standard" + ); const runsRepository = new RunsRepository({ clickhouse, prisma: this._replica as PrismaClient, @@ -148,7 +151,10 @@ export class BulkActionService extends BaseService { ...rawParams, }); - const clickhouse = await clickhouseFactory.getClickhouseForOrganization(group.project.organizationId, "standard"); + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + group.project.organizationId, + "standard" + ); const runsRepository = new RunsRepository({ clickhouse, prisma: this._replica as PrismaClient, diff --git a/apps/webapp/app/v3/services/completeAttempt.server.ts b/apps/webapp/app/v3/services/completeAttempt.server.ts index 22a9047c3f..dc51150a8e 100644 --- a/apps/webapp/app/v3/services/completeAttempt.server.ts +++ b/apps/webapp/app/v3/services/completeAttempt.server.ts @@ -563,7 +563,7 @@ export class CompleteAttemptService extends BaseService { properties: { retryAt: retryAt.toISOString(), previousMachine: oomMachine - ? taskRunAttempt.taskRun.machinePreset ?? undefined + ? (taskRunAttempt.taskRun.machinePreset ?? undefined) : undefined, nextMachine: oomMachine, }, diff --git a/apps/webapp/app/v3/services/computeTemplateCreation.server.ts b/apps/webapp/app/v3/services/computeTemplateCreation.server.ts index 8342291a32..7bc03e9d50 100644 --- a/apps/webapp/app/v3/services/computeTemplateCreation.server.ts +++ b/apps/webapp/app/v3/services/computeTemplateCreation.server.ts @@ -78,10 +78,7 @@ export class ComputeTemplateCreationService { writer?: WritableStreamDefaultWriter; }): Promise { return startActiveSpan("compute.template.create", async (span) => { - const { mode, migrated, reason } = await this.resolveMode( - options.projectId, - options.prisma - ); + const { mode, migrated, reason } = await this.resolveMode(options.projectId, options.prisma); span.setAttributes({ ...attributesFromAuthenticatedEnv(options.authenticatedEnv), diff --git a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts index 64d9786094..b5b6207a74 100644 --- a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts @@ -381,7 +381,7 @@ async function createWorkerTask( : ("STANDARD" as const); resolvedTtl = - typeof task.ttl === "number" ? stringifyDuration(task.ttl) ?? null : task.ttl ?? null; + typeof task.ttl === "number" ? (stringifyDuration(task.ttl) ?? null) : (task.ttl ?? null); await prisma.backgroundWorkerTask.create({ data: { diff --git a/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4/findOrCreateBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4/findOrCreateBackgroundWorker.server.ts index 7a2dcb06c3..d73ee46edf 100644 --- a/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4/findOrCreateBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4/findOrCreateBackgroundWorker.server.ts @@ -69,9 +69,7 @@ export async function findOrCreateBackgroundWorker( // P2002 β€” the CLI's retry will then hit `findFirst` and find the winner's row. // Intentionally NOT a ServiceValidationError so the caller doesn't fail-deploy // on a transient race. - if ( - isUniqueConstraintError(error, ["projectId", "runtimeEnvironmentId", "version"]) - ) { + if (isUniqueConstraintError(error, ["projectId", "runtimeEnvironmentId", "version"])) { throw new Error( "Concurrent background worker registration detected for this deployment version; please retry" ); diff --git a/apps/webapp/app/v3/services/createTaskRunAttempt.server.ts b/apps/webapp/app/v3/services/createTaskRunAttempt.server.ts index dbc4c576b7..0c0503b35e 100644 --- a/apps/webapp/app/v3/services/createTaskRunAttempt.server.ts +++ b/apps/webapp/app/v3/services/createTaskRunAttempt.server.ts @@ -126,8 +126,8 @@ export class CreateTaskRunAttemptService extends BaseService { const nextAttemptNumber = taskRun.attempts[0] ? taskRun.attempts[0].number + 1 : startAtZero - ? 0 - : 1; + ? 0 + : 1; if (nextAttemptNumber > MAX_TASK_RUN_ATTEMPTS) { const service = new CrashTaskRunService(this._prisma); diff --git a/apps/webapp/app/v3/services/promptService.server.ts b/apps/webapp/app/v3/services/promptService.server.ts index ab2729fc23..56e6b8fa66 100644 --- a/apps/webapp/app/v3/services/promptService.server.ts +++ b/apps/webapp/app/v3/services/promptService.server.ts @@ -158,5 +158,4 @@ export class PromptService extends BaseService { WHERE "promptId" = ${promptId} AND ${label} = ANY("labels") `; } - } diff --git a/apps/webapp/app/v3/services/taskRunConcurrencyTracker.server.ts b/apps/webapp/app/v3/services/taskRunConcurrencyTracker.server.ts index 6b9907bbab..98034761bc 100644 --- a/apps/webapp/app/v3/services/taskRunConcurrencyTracker.server.ts +++ b/apps/webapp/app/v3/services/taskRunConcurrencyTracker.server.ts @@ -253,10 +253,13 @@ class TaskRunConcurrencyTracker implements MessageQueueSubscriber { taskIds: string[] ): Promise> { const counts = await this.getTaskCounts(projectId, taskIds); - return taskIds.reduce((acc, taskId, index) => { - acc[taskId] = counts[index] ?? 0; - return acc; - }, {} as Record); + return taskIds.reduce( + (acc, taskId, index) => { + acc[taskId] = counts[index] ?? 0; + return acc; + }, + {} as Record + ); } async environmentConcurrentRunCounts( @@ -273,14 +276,17 @@ class TaskRunConcurrencyTracker implements MessageQueueSubscriber { return Object.fromEntries(environmentIds.map((id) => [id, 0])); } - return results.reduce((acc, [err, count], index) => { - if (err) { - console.error("Error in environmentConcurrentRunCounts:", err); + return results.reduce( + (acc, [err, count], index) => { + if (err) { + console.error("Error in environmentConcurrentRunCounts:", err); + return acc; + } + acc[environmentIds[index]] = count as number; return acc; - } - acc[environmentIds[index]] = count as number; - return acc; - }, {} as Record); + }, + {} as Record + ); } catch (error) { logger.error("TaskRunConcurrencyTracker.environmentConcurrentRunCounts() error", { error }); return Object.fromEntries(environmentIds.map((id) => [id, 0])); diff --git a/apps/webapp/app/v3/services/triggerTaskV1.server.ts b/apps/webapp/app/v3/services/triggerTaskV1.server.ts index a2f0b9de21..5ccee9e5c5 100644 --- a/apps/webapp/app/v3/services/triggerTaskV1.server.ts +++ b/apps/webapp/app/v3/services/triggerTaskV1.server.ts @@ -78,7 +78,7 @@ export class TriggerTaskServiceV1 extends BaseService { const ttl = typeof body.options?.ttl === "number" ? stringifyDuration(body.options?.ttl) - : body.options?.ttl ?? (environment.type === "DEVELOPMENT" ? "10m" : undefined); + : (body.options?.ttl ?? (environment.type === "DEVELOPMENT" ? "10m" : undefined)); const existingRun = idempotencyKey ? await this._prisma.taskRun.findFirst({ @@ -352,10 +352,10 @@ export class TriggerTaskServiceV1 extends BaseService { const depth = dependentAttempt ? dependentAttempt.taskRun.depth + 1 : parentAttempt - ? parentAttempt.taskRun.depth + 1 - : dependentBatchRun?.dependentTaskAttempt - ? dependentBatchRun.dependentTaskAttempt.taskRun.depth + 1 - : 0; + ? parentAttempt.taskRun.depth + 1 + : dependentBatchRun?.dependentTaskAttempt + ? dependentBatchRun.dependentTaskAttempt.taskRun.depth + 1 + : 0; const queueTimestamp = options.queueTimestamp ?? @@ -738,7 +738,12 @@ export class TriggerTaskServiceV1 extends BaseService { const filename = `${pathPrefix}/payload.json`; - const uploadedFilename = await uploadPacketToObjectStore(filename, packet.data, packet.dataType, environment); + const uploadedFilename = await uploadPacketToObjectStore( + filename, + packet.data, + packet.dataType, + environment + ); return { data: uploadedFilename, diff --git a/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts b/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts index 7f52e32d5c..da1caa866e 100644 --- a/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts +++ b/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts @@ -1,4 +1,9 @@ -import { createCache, createLRUMemoryStore, DefaultStatefulContext, Namespace } from "@internal/cache"; +import { + createCache, + createLRUMemoryStore, + DefaultStatefulContext, + Namespace, +} from "@internal/cache"; import { CheckpointInput, CompleteRunAttemptResult, diff --git a/apps/webapp/app/v3/taskEventStore.server.ts b/apps/webapp/app/v3/taskEventStore.server.ts index ed580b40d0..093f127132 100644 --- a/apps/webapp/app/v3/taskEventStore.server.ts +++ b/apps/webapp/app/v3/taskEventStore.server.ts @@ -56,7 +56,10 @@ export function getTaskEventStore(): TaskEventStoreTable { } export class TaskEventStore { - constructor(private db: PrismaClient, private readReplica: PrismaReplicaClient) {} + constructor( + private db: PrismaClient, + private readReplica: PrismaReplicaClient + ) {} /** * Insert one record. diff --git a/apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts b/apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts index 024ac75a94..9173038db0 100644 --- a/apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts +++ b/apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts @@ -163,19 +163,22 @@ function enrichLlmMetrics(event: CreateEventInput): void { // v6 emits ai.response.finishReason (plain string); v7 (@ai-sdk/otel) emits // gen_ai.response.finish_reasons as a JSON array string (e.g. `["stop"]`). const finishReason = readFinishReason(props); - const operationId = typeof props["ai.operationId"] === "string" - ? props["ai.operationId"] - : typeof props["gen_ai.operation.name"] === "string" - ? props["gen_ai.operation.name"] - : typeof props["operation.name"] === "string" - ? props["operation.name"] - : ""; - const msToFirstChunk = typeof props["ai.response.msToFirstChunk"] === "number" - ? props["ai.response.msToFirstChunk"] - : 0; - const avgTokensPerSec = typeof props["ai.response.avgOutputTokensPerSecond"] === "number" - ? props["ai.response.avgOutputTokensPerSecond"] - : 0; + const operationId = + typeof props["ai.operationId"] === "string" + ? props["ai.operationId"] + : typeof props["gen_ai.operation.name"] === "string" + ? props["gen_ai.operation.name"] + : typeof props["operation.name"] === "string" + ? props["operation.name"] + : ""; + const msToFirstChunk = + typeof props["ai.response.msToFirstChunk"] === "number" + ? props["ai.response.msToFirstChunk"] + : 0; + const avgTokensPerSec = + typeof props["ai.response.avgOutputTokensPerSecond"] === "number" + ? props["ai.response.avgOutputTokensPerSecond"] + : 0; const costSource = cost ? "registry" : providerCost ? providerCost.source : ""; const providerCostValue = providerCost?.totalCost ?? 0; @@ -187,7 +190,10 @@ function enrichLlmMetrics(event: CreateEventInput): void { : typeof props["gen_ai.provider.name"] === "string" ? props["gen_ai.provider.name"] : "unknown", - requestModel: typeof props["gen_ai.request.model"] === "string" ? props["gen_ai.request.model"] : responseModel, + requestModel: + typeof props["gen_ai.request.model"] === "string" + ? props["gen_ai.request.model"] + : responseModel, responseModel, baseResponseModel: modelCatalog[responseModel]?.baseModelName ?? responseModel, matchedModelId: cost?.matchedModelId ?? "", @@ -195,10 +201,12 @@ function enrichLlmMetrics(event: CreateEventInput): void { finishReason, costSource, pricingTierId: cost?.pricingTierId ?? (providerCost ? `provider:${providerCost.source}` : ""), - pricingTierName: cost?.pricingTierName ?? (providerCost ? `${providerCost.source} reported` : ""), + pricingTierName: + cost?.pricingTierName ?? (providerCost ? `${providerCost.source} reported` : ""), inputTokens: usageDetails["input"] ?? 0, outputTokens: usageDetails["output"] ?? 0, - totalTokens: usageDetails["total"] ?? (usageDetails["input"] ?? 0) + (usageDetails["output"] ?? 0), + totalTokens: + usageDetails["total"] ?? (usageDetails["input"] ?? 0) + (usageDetails["output"] ?? 0), usageDetails, inputCost: cost?.inputCost ?? 0, outputCost: cost?.outputCost ?? 0, diff --git a/apps/webapp/app/v3/utils/zodPubSub.server.ts b/apps/webapp/app/v3/utils/zodPubSub.server.ts index 2c081bc368..264be04894 100644 --- a/apps/webapp/app/v3/utils/zodPubSub.server.ts +++ b/apps/webapp/app/v3/utils/zodPubSub.server.ts @@ -20,9 +20,9 @@ export interface ZodSubscriber stopListening(): Promise; } -class RedisZodSubscriber - implements ZodSubscriber -{ +class RedisZodSubscriber< + TMessageCatalog extends ZodMessageCatalogSchema, +> implements ZodSubscriber { private _subscriber: RedisClient; private _listeners: Map Promise> = new Map(); private _messageHandler: ZodMessageHandler; diff --git a/apps/webapp/app/v3/vercel/index.ts b/apps/webapp/app/v3/vercel/index.ts index f34f0b64c6..39be3f97d1 100644 --- a/apps/webapp/app/v3/vercel/index.ts +++ b/apps/webapp/app/v3/vercel/index.ts @@ -13,5 +13,3 @@ export function getVercelInstallParams(request: Request) { return null; } - - diff --git a/apps/webapp/app/v3/vercel/vercelOAuthState.server.ts b/apps/webapp/app/v3/vercel/vercelOAuthState.server.ts index 31f42acc87..e6bfbb0362 100644 --- a/apps/webapp/app/v3/vercel/vercelOAuthState.server.ts +++ b/apps/webapp/app/v3/vercel/vercelOAuthState.server.ts @@ -12,9 +12,7 @@ export const VercelOAuthStateSchema = z.object({ export type VercelOAuthState = z.infer; -export async function generateVercelOAuthState( - params: VercelOAuthState -): Promise { +export async function generateVercelOAuthState(params: VercelOAuthState): Promise { return generateJWT({ secretKey: env.ENCRYPTION_KEY, payload: params, diff --git a/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts b/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts index 95cff1a480..1399b87fc2 100644 --- a/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts +++ b/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts @@ -15,38 +15,47 @@ 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 - ); -}); +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. * Parses the string as JSON and validates each element is a valid EnvSlug. * Invalid elements are filtered out rather than rejecting the whole array. */ -export const envSlugArrayField = z.string().optional().transform((val): EnvSlug[] | null => { - if (!val) return null; - return safeJsonParse(val).match( - (parsed) => { - if (!Array.isArray(parsed)) return null; - return parsed.filter((item): item is EnvSlug => EnvSlugSchema.safeParse(item).success); - }, - () => null - ); -}); +export const envSlugArrayField = z + .string() + .optional() + .transform((val): EnvSlug[] | null => { + if (!val) return null; + return safeJsonParse(val).match( + (parsed) => { + if (!Array.isArray(parsed)) return null; + return parsed.filter((item): item is EnvSlug => EnvSlugSchema.safeParse(item).success); + }, + () => null + ); + }); export 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. */ - vercelStagingEnvironment: z.object({ - environmentId: z.string(), - displayName: z.string(), - }).nullable().optional(), + vercelStagingEnvironment: z + .object({ + environmentId: z.string(), + displayName: z.string(), + }) + .nullable() + .optional(), discoverEnvVars: z.array(EnvSlugSchema).nullable().optional(), autoPromote: z.boolean().optional().default(true), }); @@ -61,7 +70,9 @@ 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.record(EnvSlugSchema, z.record(z.string(), z.boolean())).default({}); +export const SyncEnvVarsMappingSchema = z + .record(EnvSlugSchema, z.record(z.string(), z.boolean())) + .default({}); export type SyncEnvVarsMapping = z.infer; @@ -135,7 +146,9 @@ export function getAvailableEnvSlugsForBuildSettings( hasStagingEnvironment: boolean, hasPreviewEnvironment: boolean ): EnvSlug[] { - return getAvailableEnvSlugs(hasStagingEnvironment, hasPreviewEnvironment).filter((s) => s !== "dev"); + return getAvailableEnvSlugs(hasStagingEnvironment, hasPreviewEnvironment).filter( + (s) => s !== "dev" + ); } export function isDiscoverEnvVarsEnabledForEnvironment( diff --git a/apps/webapp/app/v3/vercel/vercelUrls.server.ts b/apps/webapp/app/v3/vercel/vercelUrls.server.ts index 957e0d2907..9656148a11 100644 --- a/apps/webapp/app/v3/vercel/vercelUrls.server.ts +++ b/apps/webapp/app/v3/vercel/vercelUrls.server.ts @@ -12,10 +12,7 @@ export function sanitizeVercelNextUrl(url: string | undefined | null): string | try { const parsed = new URL(url); - if ( - parsed.protocol === "https:" && - /^([a-z0-9-]+\.)*vercel\.com$/i.test(parsed.hostname) - ) { + if (parsed.protocol === "https:" && /^([a-z0-9-]+\.)*vercel\.com$/i.test(parsed.hostname)) { return parsed.toString(); } } catch { diff --git a/apps/webapp/app/v3/workerRegions.server.ts b/apps/webapp/app/v3/workerRegions.server.ts index 5e86f1dd46..f682868e55 100644 --- a/apps/webapp/app/v3/workerRegions.server.ts +++ b/apps/webapp/app/v3/workerRegions.server.ts @@ -39,10 +39,7 @@ export function backingForQueue( if (!region) return undefined; const backing = groups.find( (g) => - g.workloadType === "MICROVM" && - g.region === region && - !g.hidden && - g.masterQueue !== queue + g.workloadType === "MICROVM" && g.region === region && !g.hidden && g.masterQueue !== queue ); if (!backing) return undefined; return { workerQueue: backing.masterQueue, enableFastPath: backing.enableFastPath }; diff --git a/apps/webapp/evals/aiRunFilter.eval.ts b/apps/webapp/evals/aiRunFilter.eval.ts index 28c49eceb1..265e42da24 100644 --- a/apps/webapp/evals/aiRunFilter.eval.ts +++ b/apps/webapp/evals/aiRunFilter.eval.ts @@ -232,8 +232,12 @@ evalite("AI Run Filter", { expected: JSON.stringify({ success: true, filters: { - from: new Date(new Date(Date.now() - 24*60*60*1000).toDateString() + " 14:00:00").getTime(), - to: new Date(new Date(Date.now() - 24*60*60*1000).toDateString() + " 14:59:59").getTime(), + from: new Date( + new Date(Date.now() - 24 * 60 * 60 * 1000).toDateString() + " 14:00:00" + ).getTime(), + to: new Date( + new Date(Date.now() - 24 * 60 * 60 * 1000).toDateString() + " 14:59:59" + ).getTime(), }, }), }, diff --git a/apps/webapp/package.json b/apps/webapp/package.json index c5a4ae5787..922837b756 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -10,8 +10,8 @@ "build:sentry": "esbuild --platform=node --format=cjs --outbase=. ./sentry.server.ts ./app/utils/sentryTraceContext.server.ts --outdir=build --sourcemap", "dev": "cross-env PORT=3030 remix dev -c \"node ./build/server.js\"", "dev:worker": "cross-env NODE_PATH=../../node_modules/.pnpm/node_modules node ./build/server.js", - "format": "prettier --write .", - "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .", + "format": "oxfmt .", + "lint": "oxlint -c ../../.oxlintrc.json", "start": "cross-env NODE_ENV=production node --max-old-space-size=8192 ./build/server.js", "start:local": "cross-env node --max-old-space-size=8192 ./build/server.js", "typecheck": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsc --noEmit -p ./tsconfig.check.json", @@ -21,11 +21,6 @@ "test": "vitest --no-file-parallelism", "eval:dev": "evalite watch" }, - "eslintIgnore": [ - "/node_modules", - "/build", - "/public/build" - ], "dependencies": { "@ai-sdk/anthropic": "^3.0.0", "@ai-sdk/openai": "^3.0.0", @@ -248,7 +243,6 @@ "@internal/replication": "workspace:*", "@internal/testcontainers": "workspace:*", "@remix-run/dev": "2.17.4", - "@remix-run/eslint-config": "2.17.4", "@remix-run/testing": "^2.17.4", "@sentry/cli": "2.50.2", "@swc/core": "^1.3.4", @@ -259,7 +253,6 @@ "@types/bcryptjs": "^2.4.2", "@types/compression": "^1.7.2", "@types/cookie": "^0.6.0", - "@types/eslint": "^8.4.6", "@types/express": "^4.17.13", "@types/humanize-duration": "^3.27.1", "@types/json-query": "^2.2.3", @@ -280,25 +273,16 @@ "@types/supertest": "^6.0.2", "@types/tar": "^6.1.4", "@types/ws": "^8.5.3", - "@typescript-eslint/eslint-plugin": "^5.59.6", - "@typescript-eslint/parser": "^5.59.6", "autoevals": "^0.0.130", "autoprefixer": "^10.4.13", "css-loader": "^6.10.0", "datepicker": "link:@types/@react-aria/datepicker", "engine.io": "^6.5.4", "esbuild": "^0.15.10", - "eslint": "^8.24.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-turbo": "^2.0.4", "evalite": "1.0.0-beta.16", "npm-run-all": "^4.1.5", "postcss-import": "^16.0.1", "postcss-loader": "^8.1.1", - "prettier": "^2.8.8", - "prettier-plugin-tailwindcss": "^0.3.0", "prop-types": "^15.8.1", "rimraf": "^6.0.1", "style-loader": "^3.3.4", diff --git a/apps/webapp/prettier.config.js b/apps/webapp/prettier.config.js deleted file mode 100644 index f652d8bf75..0000000000 --- a/apps/webapp/prettier.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - ...require("../../prettier.config.js"), - plugins: [require("prettier-plugin-tailwindcss")], -}; diff --git a/apps/webapp/seed-ai-spans.mts b/apps/webapp/seed-ai-spans.mts index 6ada9fb328..5297171f95 100644 --- a/apps/webapp/seed-ai-spans.mts +++ b/apps/webapp/seed-ai-spans.mts @@ -3,10 +3,7 @@ import { createOrganization } from "./app/models/organization.server"; import { createProject } from "./app/models/project.server"; import { ClickHouse } from "@internal/clickhouse"; import type { TaskEventV2Input, LlmMetricsV1Input } from "@internal/clickhouse"; -import { - generateTraceId, - generateSpanId, -} from "./app/v3/eventRepository/common.server"; +import { generateTraceId, generateSpanId } from "./app/v3/eventRepository/common.server"; import { enrichCreatableEvents, setLlmPricingRegistry, @@ -24,9 +21,18 @@ const QUEUE_NAME = "task/ai-chat"; const WORKER_VERSION = "seed-ai-spans-v1"; const SEED_USER_IDS = [ - "user_alice", "user_bob", "user_carol", "user_dave", - "user_eve", "user_frank", "user_grace", "user_heidi", - "user_ivan", "user_judy", "user_karl", "user_liam", + "user_alice", + "user_bob", + "user_carol", + "user_dave", + "user_eve", + "user_frank", + "user_grace", + "user_heidi", + "user_ivan", + "user_judy", + "user_karl", + "user_liam", ]; function randomUserId(): string { @@ -80,8 +86,7 @@ function eventToClickhouseRow(event: CreateEventInput): TaskEventV2Input { // attributes const publicAttrs = removePrivateProperties(event.properties as Attributes); const unflattened = publicAttrs ? unflattenAttributes(publicAttrs) : {}; - const attributes = - unflattened && typeof unflattened === "object" ? { ...unflattened } : {}; + const attributes = unflattened && typeof unflattened === "object" ? { ...unflattened } : {}; // metadata β€” mirrors createEventToTaskEventV1InputMetadata const metadataObj: Record = {}; @@ -118,9 +123,7 @@ function eventToClickhouseRow(event: CreateEventInput): TaskEventV2Input { status, attributes, metadata, - expires_at: formatClickhouseDateTime( - new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) - ), + expires_at: formatClickhouseDateTime(new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)), machine_id: "", }; } @@ -386,9 +389,7 @@ Please structure your response with clear headings, use tables for comparative d const enrichedCount = enriched.filter((e) => e._llmMetrics != null).length; const totalCost = enriched.reduce((sum, e) => sum + (e._llmMetrics?.totalCost ?? 0), 0); - console.log( - `Enriched ${enrichedCount} spans with LLM cost (total: $${totalCost.toFixed(6)})` - ); + console.log(`Enriched ${enrichedCount} spans with LLM cost (total: $${totalCost.toFixed(6)})`); // 11. Insert into ClickHouse const clickhouseUrl = process.env.CLICKHOUSE_URL ?? process.env.EVENTS_CLICKHOUSE_URL; @@ -844,8 +845,7 @@ Cut (-25 bps): 10.7% "ai.response.finishReason": "tool-calls", "ai.response.id": "msg_seed_003", "ai.response.model": "claude-haiku-4-5-20251001", - "ai.response.text": - "I'll search for the latest Federal Reserve interest rate information.", + "ai.response.text": "I'll search for the latest Federal Reserve interest rate information.", "ai.response.toolCalls": JSON.stringify([ { toolCallId: "toolu_seed_001", @@ -1099,7 +1099,15 @@ Cut (-25 bps): 10.7% } } - events.push(makeEvent({ message: opts.wrapperMsg, spanId: wId, parentId: runFnId, ...wrap, properties: wrapperProps })); + events.push( + makeEvent({ + message: opts.wrapperMsg, + spanId: wId, + parentId: runFnId, + ...wrap, + properties: wrapperProps, + }) + ); cursor = wrap.startMs + 50; const doTiming = next(opts.doDurationMs); @@ -1159,29 +1167,45 @@ Cut (-25 bps): 10.7% } if (opts.extraDoProps) Object.assign(doProps, opts.extraDoProps); - events.push(makeEvent({ message: opts.doMsg, spanId: dId, parentId: wId, ...doTiming, properties: doProps })); + events.push( + makeEvent({ + message: opts.doMsg, + spanId: dId, + parentId: wId, + ...doTiming, + properties: doProps, + }) + ); return { wrapperId: wId, doId: dId }; } // Helper: add a tool call span - function addToolCall(parentId: string, name: string, args: string, result: string, durationMs = 500) { + function addToolCall( + parentId: string, + name: string, + args: string, + result: string, + durationMs = 500 + ) { const id = generateSpanId(); const timing = next(durationMs); - events.push(makeEvent({ - message: "ai.toolCall", - spanId: id, - parentId, - ...timing, - properties: { - "ai.operationId": "ai.toolCall", - "ai.toolCall.name": name, - "ai.toolCall.id": `call_${generateSpanId().slice(0, 8)}`, - "ai.toolCall.args": args, - "ai.toolCall.result": result, - "operation.name": "ai.toolCall", - }, - })); + events.push( + makeEvent({ + message: "ai.toolCall", + spanId: id, + parentId, + ...timing, + properties: { + "ai.operationId": "ai.toolCall", + "ai.toolCall.name": name, + "ai.toolCall.id": `call_${generateSpanId().slice(0, 8)}`, + "ai.toolCall.args": args, + "ai.toolCall.result": result, + "operation.name": "ai.toolCall", + }, + }) + ); return id; } @@ -1253,7 +1277,8 @@ The document primarily discusses **quarterly earnings guidance** for the technol canonicalSlug: "openai/gpt-5-mini", finalProvider: "openai", fallbacksAvailable: ["azure"], - planningReasoning: "System credentials planned for: openai, azure. Total execution order: openai(system) β†’ azure(system)", + planningReasoning: + "System credentials planned for: openai, azure. Total execution order: openai(system) β†’ azure(system)", modelAttemptCount: 1, }, cost: "0.000482", @@ -1293,7 +1318,12 @@ The document primarily discusses **quarterly earnings guidance** for the technol }, }, }); - addToolCall(ds.wrapperId, "classifyContent", '{"text":"Federal Reserve rate analysis"}', '{"category":"finance","confidence":0.98}'); + addToolCall( + ds.wrapperId, + "classifyContent", + '{"text":"Federal Reserve rate analysis"}', + '{"category":"finance","confidence":0.98}' + ); // ===================================================================== // 8) Gateway β†’ Anthropic claude-haiku via gateway prefix @@ -1354,7 +1384,10 @@ The document primarily discusses **quarterly earnings guidance** for the technol finishReason: "stop", wrapperDurationMs: 1_200, doDurationMs: 1_000, - responseObject: JSON.stringify({ sentiment: "neutral", topics: ["monetary_policy", "interest_rates"] }), + responseObject: JSON.stringify({ + sentiment: "neutral", + topics: ["monetary_policy", "interest_rates"], + }), useCompletionStyle: true, providerMetadata: { gateway: { @@ -1495,16 +1528,23 @@ The next decision point will hinge on incoming data, particularly: wrapperDurationMs: 3_500, doDurationMs: 3_000, responseText: "Let me look up the latest rate decision.", - toolCallsJson: JSON.stringify([{ - toolCallId: "call_azure_001", - toolName: "lookupRate", - input: '{"source":"federal_reserve","metric":"funds_rate"}', - }]), + toolCallsJson: JSON.stringify([ + { + toolCallId: "call_azure_001", + toolName: "lookupRate", + input: '{"source":"federal_reserve","metric":"funds_rate"}', + }, + ]), providerMetadata: { azure: { responseId: "resp_seed_azure_001", serviceTier: "default" }, }, }); - addToolCall(az.wrapperId, "lookupRate", '{"source":"federal_reserve","metric":"funds_rate"}', '{"rate":"4.25-4.50%","effectiveDate":"2024-12-18"}'); + addToolCall( + az.wrapperId, + "lookupRate", + '{"source":"federal_reserve","metric":"funds_rate"}', + '{"rate":"4.25-4.50%","effectiveDate":"2024-12-18"}' + ); // ===================================================================== // 14) Perplexity β†’ sonar-pro @@ -1605,7 +1645,8 @@ The FOMC has cited three primary factors: ### Technical Note The effective federal funds rate (\`EFFR\`) currently sits at **4.33%**, near the midpoint of the target range. The overnight reverse repo facility (ON RRP) rate is set at **4.25%**.`, - responseReasoning: "The user is asking about the current Federal Reserve interest rate. Let me provide a comprehensive answer based on the most recent FOMC decision. I should include context about the rate trajectory and forward guidance. I'll structure this with a table showing the recent rate changes and explain the pause rationale.", + responseReasoning: + "The user is asking about the current Federal Reserve interest rate. Let me provide a comprehensive answer based on the most recent FOMC decision. I should include context about the rate trajectory and forward guidance. I'll structure this with a table showing the recent rate changes and explain the pause rationale.", cacheReadTokens: 12400, cacheCreationTokens: 2800, providerMetadata: { @@ -1637,11 +1678,13 @@ The effective federal funds rate (\`EFFR\`) currently sits at **4.33%**, near th wrapperDurationMs: 6_000, doDurationMs: 5_500, responseText: "I'll search for the latest FOMC decision and rate information.", - toolCallsJson: JSON.stringify([{ - toolCallId: "call_vertex_001", - toolName: "searchFOMC", - input: '{"query":"latest FOMC decision december 2024"}', - }]), + toolCallsJson: JSON.stringify([ + { + toolCallId: "call_vertex_001", + toolName: "searchFOMC", + input: '{"query":"latest FOMC decision december 2024"}', + }, + ]), providerMetadata: { google: { usageMetadata: { @@ -1653,7 +1696,13 @@ The effective federal funds rate (\`EFFR\`) currently sits at **4.33%**, near th }, }, }); - addToolCall(vt.wrapperId, "searchFOMC", '{"query":"latest FOMC decision december 2024"}', '{"decision":"hold","rate":"4.25-4.50%","date":"2024-12-18","vote":"unanimous"}', 800); + addToolCall( + vt.wrapperId, + "searchFOMC", + '{"query":"latest FOMC decision december 2024"}', + '{"decision":"hold","rate":"4.25-4.50%","date":"2024-12-18","vote":"unanimous"}', + 800 + ); // ===================================================================== // 18) openai.responses β†’ gpt-5.4 with reasoning tokens @@ -1704,7 +1753,8 @@ rates = { | Global growth slowdown | ↓ Downside | Medium | > *"The committee remains attentive to the risks to both sides of its dual mandate."* β€” Chair Powell, Dec 18 press conference`, - responseReasoning: "I need to provide accurate, up-to-date information about the Federal Reserve interest rate. The last FOMC meeting was in December 2024 where they cut rates by 25 bps after two previous cuts. Let me include the dot plot projections and a risk assessment table for a comprehensive view.", + responseReasoning: + "I need to provide accurate, up-to-date information about the Federal Reserve interest rate. The last FOMC meeting was in December 2024 where they cut rates by 25 bps after two previous cuts. Let me include the dot plot projections and a risk assessment table for a comprehensive view.", reasoningTokens: 516, providerMetadata: { openai: { diff --git a/apps/webapp/seed.mts b/apps/webapp/seed.mts index 2571f8679d..7e67e616e9 100644 --- a/apps/webapp/seed.mts +++ b/apps/webapp/seed.mts @@ -294,9 +294,7 @@ async function ensureDefaultWorkerGroup() { console.log(`βœ… Created worker instance group: ${workerGroup.name} (${workerGroup.id})`); } else { - console.log( - `βœ… Worker instance group already exists: ${workerGroup.name} (${workerGroup.id})` - ); + console.log(`βœ… Worker instance group already exists: ${workerGroup.name} (${workerGroup.id})`); } // Set the feature flag diff --git a/apps/webapp/test/EnvironmentVariablesPresenter.test.ts b/apps/webapp/test/EnvironmentVariablesPresenter.test.ts index b5172bd594..ba4daea89d 100644 --- a/apps/webapp/test/EnvironmentVariablesPresenter.test.ts +++ b/apps/webapp/test/EnvironmentVariablesPresenter.test.ts @@ -11,9 +11,7 @@ vi.mock("~/db.server", () => ({ fnOrOptions?: ((tx: unknown) => Promise) | unknown ) => { const fn = - typeof nameOrFn === "string" - ? (fnOrOptions as (tx: unknown) => Promise) - : nameOrFn; + typeof nameOrFn === "string" ? (fnOrOptions as (tx: unknown) => Promise) : nameOrFn; return prismaClient.$transaction(fn); }, @@ -31,44 +29,52 @@ import { vi.setConfig({ testTimeout: 60_000 }); describe("EnvironmentVariablesPresenter", () => { - postgresTest("keeps secret values redacted while returning non-secret values", async ({ prisma }) => { - const { user, organization, project, projectSlug } = await createTestOrgProjectWithMember(prisma); - const production = await createRuntimeEnvironment(prisma, { - projectId: project.id, - organizationId: organization.id, - type: "PRODUCTION", - }); - - const repository = new EnvironmentVariablesRepository(prisma, prisma); - - await createEnvironmentVariable(repository, project.id, { - environmentId: production.id, - key: "SECRET_VAR", - value: "super-secret", - isSecret: true, - userId: user.id, - }); - await createEnvironmentVariable(repository, project.id, { - environmentId: production.id, - key: "PLAIN_VAR", - value: "plain-value", - isSecret: false, - userId: user.id, - }); - - const result = await new EnvironmentVariablesPresenter(prisma, prisma).call({ - userId: user.id, - projectSlug, - }); - - const secretVariable = result.environmentVariables.find((variable) => variable.key === "SECRET_VAR"); - const nonSecretVariable = result.environmentVariables.find((variable) => variable.key === "PLAIN_VAR"); - - expect(secretVariable).toBeDefined(); - expect(nonSecretVariable).toBeDefined(); - expect(secretVariable!.value).toBe(""); - expect(nonSecretVariable!.value).toBe("plain-value"); - }); + postgresTest( + "keeps secret values redacted while returning non-secret values", + async ({ prisma }) => { + const { user, organization, project, projectSlug } = + await createTestOrgProjectWithMember(prisma); + const production = await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PRODUCTION", + }); + + const repository = new EnvironmentVariablesRepository(prisma, prisma); + + await createEnvironmentVariable(repository, project.id, { + environmentId: production.id, + key: "SECRET_VAR", + value: "super-secret", + isSecret: true, + userId: user.id, + }); + await createEnvironmentVariable(repository, project.id, { + environmentId: production.id, + key: "PLAIN_VAR", + value: "plain-value", + isSecret: false, + userId: user.id, + }); + + const result = await new EnvironmentVariablesPresenter(prisma, prisma).call({ + userId: user.id, + projectSlug, + }); + + const secretVariable = result.environmentVariables.find( + (variable) => variable.key === "SECRET_VAR" + ); + const nonSecretVariable = result.environmentVariables.find( + (variable) => variable.key === "PLAIN_VAR" + ); + + expect(secretVariable).toBeDefined(); + expect(nonSecretVariable).toBeDefined(); + expect(secretVariable!.value).toBe(""); + expect(nonSecretVariable!.value).toBe("plain-value"); + } + ); postgresTest( "returns values for active environments (including branch environments) and excludes archived branch environments", diff --git a/apps/webapp/test/api-auth.e2e.test.ts b/apps/webapp/test/api-auth.e2e.test.ts index 31e365d6d4..9e0cc2b55b 100644 --- a/apps/webapp/test/api-auth.e2e.test.ts +++ b/apps/webapp/test/api-auth.e2e.test.ts @@ -177,7 +177,6 @@ describe("API bearer auth β€” action requests", () => { }); expect(res.status).toBe(401); }); - }); describe("JWT bearer auth β€” action requests", () => { @@ -353,7 +352,7 @@ describe("JWT bearer auth β€” resource-scoped scopes", () => { // - multi-key resource callbacks (runs/tags/batch/tasks) β€” any key match grants access // - empty resource callbacks relying on superScopes describe("JWT bearer auth β€” behaviours to preserve through TRI-8719", () => { - it("custom action: type-level write:tasks scope satisfies action=\"trigger\" (auth passes)", async () => { + it('custom action: type-level write:tasks scope satisfies action="trigger" (auth passes)', async () => { const { environment } = await seedTestEnvironment(server.prisma); // Current SDK + MCP JWTs for task-trigger use type-level scope, e.g. write:tasks. // Legacy checkAuthorization passes via exact superScope match ["write:tasks", "admin"]. diff --git a/apps/webapp/test/auth-api.e2e.full.test.ts b/apps/webapp/test/auth-api.e2e.full.test.ts index e66cb1e807..279cbeb98f 100644 --- a/apps/webapp/test/auth-api.e2e.full.test.ts +++ b/apps/webapp/test/auth-api.e2e.full.test.ts @@ -160,8 +160,7 @@ describe("API", () => { // Plus the equivalent full matrix for input-streams which the smoke // matrix doesn't touch. describe("Resource-scoped writes β€” waitpoints (gap-fill)", () => { - const pathFor = (friendlyId: string) => - `/api/v1/waitpoints/tokens/${friendlyId}/complete`; + const pathFor = (friendlyId: string) => `/api/v1/waitpoints/tokens/${friendlyId}/complete`; const completeRequest = (path: string, headers: Record) => getTestServer().webapp.fetch(path, { method: "POST", @@ -941,10 +940,9 @@ describe("API", () => { }, expirationTime: "15m", }); - const res = await get( - "?filter%5BtaskIdentifier%5D=task_a%2Ctask_b", - { Authorization: `Bearer ${jwt}` } - ); + const res = await get("?filter%5BtaskIdentifier%5D=task_a%2Ctask_b", { + Authorization: `Bearer ${jwt}`, + }); // Resource array is [{type:"runs"}, {type:"tasks",id:"task_a"}, {type:"tasks",id:"task_b"}]. // The scope read:tasks:task_a matches the second element β†’ access granted. // Handler may 500 (ClickHouse unreachable in tests) but auth passed. @@ -964,10 +962,9 @@ describe("API", () => { }, expirationTime: "15m", }); - const res = await get( - "?filter%5BtaskIdentifier%5D=task_a", - { Authorization: `Bearer ${jwt}` } - ); + const res = await get("?filter%5BtaskIdentifier%5D=task_a", { + Authorization: `Bearer ${jwt}`, + }); // Resource is [{runs}, {tasks:task_a}]. JWT scope says // read:tasks:task_z which doesn't match the runs collection // (wrong type) or the task_a element (wrong id). 403. @@ -1767,10 +1764,9 @@ describe("API", () => { payload: { pub: true, sub: seed.environment.id, scopes: ["read:batch"] }, expirationTime: "15m", }); - const res = await getTestServer().webapp.fetch( - `/api/v2/batches/${seeded.batchFriendlyId}`, - { headers: { Authorization: `Bearer ${jwt}` } } - ); + const res = await getTestServer().webapp.fetch(`/api/v2/batches/${seeded.batchFriendlyId}`, { + headers: { Authorization: `Bearer ${jwt}` }, + }); expect(res.status).not.toBe(401); expect(res.status).not.toBe(403); }); @@ -2064,14 +2060,11 @@ describe("API", () => { }); // Body must satisfy the route's schema ({ version: positive int }) // β€” otherwise body validation 400s before authorization runs. - const res = await server.webapp.fetch( - "/api/v1/prompts/some-slug/override/reactivate", - { - method: "POST", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwt}` }, - body: JSON.stringify({ version: 1 }), - } - ); + const res = await server.webapp.fetch("/api/v1/prompts/some-slug/override/reactivate", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwt}` }, + body: JSON.stringify({ version: 1 }), + }); expect(res.status).toBe(403); }); }); @@ -2252,9 +2245,12 @@ describe("API", () => { payload: { pub: true, sub: seed.environment.id, scopes: ["read:query:runs"] }, expirationTime: "15m", }); - const res = await post({ query: "SELECT * FROM runs" }, { - Authorization: `Bearer ${jwt}`, - }); + const res = await post( + { query: "SELECT * FROM runs" }, + { + Authorization: `Bearer ${jwt}`, + } + ); expect(res.status).not.toBe(401); expect(res.status).not.toBe(403); }); @@ -2290,8 +2286,7 @@ describe("API", () => { }); const res = await post( { - query: - "SELECT count() FROM runs UNION ALL SELECT count() FROM metrics", + query: "SELECT count() FROM runs UNION ALL SELECT count() FROM metrics", }, { Authorization: `Bearer ${jwt}` } ); @@ -2314,8 +2309,7 @@ describe("API", () => { }); const res = await post( { - query: - "SELECT count() FROM runs UNION ALL SELECT count() FROM metrics", + query: "SELECT count() FROM runs UNION ALL SELECT count() FROM metrics", }, { Authorization: `Bearer ${jwt}` } ); @@ -2335,9 +2329,12 @@ describe("API", () => { }, expirationTime: "15m", }); - const res = await post({ query: "SELECT * FROM runs" }, { - Authorization: `Bearer ${jwt}`, - }); + const res = await post( + { query: "SELECT * FROM runs" }, + { + Authorization: `Bearer ${jwt}`, + } + ); expect(res.status).toBe(403); }); @@ -2349,9 +2346,12 @@ describe("API", () => { payload: { pub: true, sub: seed.environment.id, scopes: ["admin"] }, expirationTime: "15m", }); - const res = await post({ query: "SELECT * FROM runs" }, { - Authorization: `Bearer ${jwt}`, - }); + const res = await post( + { query: "SELECT * FROM runs" }, + { + Authorization: `Bearer ${jwt}`, + } + ); expect(res.status).not.toBe(401); expect(res.status).not.toBe(403); }); @@ -2422,9 +2422,7 @@ describe("API", () => { // Old superScopes: ["read:sessions", "read:all", "admin"] describe("List sessions β€” GET /api/v1/sessions", () => { const path = (taskFilter?: string) => - taskFilter - ? `/api/v1/sessions?filter[taskIdentifier]=${taskFilter}` - : "/api/v1/sessions"; + taskFilter ? `/api/v1/sessions?filter[taskIdentifier]=${taskFilter}` : "/api/v1/sessions"; const fetchWithJwt = async (jwt: string, taskFilter?: string) => getTestServer().webapp.fetch(path(taskFilter), { @@ -2445,9 +2443,7 @@ describe("API", () => { it("read:tasks:foo on filter=foo: auth passes", async () => { const seed = await seedTestEnvironment(getTestServer().prisma); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "read:tasks:foo", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["read:tasks:foo"]); const res = await fetchWithJwt(jwt, "foo"); expect(res.status).not.toBe(401); expect(res.status).not.toBe(403); @@ -2455,18 +2451,14 @@ describe("API", () => { it("read:tasks:bar on filter=foo: 403 (per-task narrowing)", async () => { const seed = await seedTestEnvironment(getTestServer().prisma); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "read:tasks:bar", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["read:tasks:bar"]); const res = await fetchWithJwt(jwt, "foo"); expect(res.status).toBe(403); }); it("read:sessions on filter=foo: auth passes (was a superScope)", async () => { const seed = await seedTestEnvironment(getTestServer().prisma); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "read:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["read:sessions"]); const res = await fetchWithJwt(jwt, "foo"); expect(res.status).not.toBe(401); expect(res.status).not.toBe(403); @@ -2474,9 +2466,7 @@ describe("API", () => { it("read:sessions on no-filter list: auth passes", async () => { const seed = await seedTestEnvironment(getTestServer().prisma); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "read:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["read:sessions"]); const res = await fetchWithJwt(jwt); expect(res.status).not.toBe(401); expect(res.status).not.toBe(403); @@ -2502,18 +2492,14 @@ describe("API", () => { // No filter β†’ resource is `{ type: "sessions" }` only. read:tasks // doesn't match the sessions type, so 403 β€” explicit narrowing. const seed = await seedTestEnvironment(getTestServer().prisma); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "read:tasks", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["read:tasks"]); const res = await fetchWithJwt(jwt); expect(res.status).toBe(403); }); it("write:tasks:foo (wrong action) on filter=foo: 403", async () => { const seed = await seedTestEnvironment(getTestServer().prisma); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "write:tasks:foo", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["write:tasks:foo"]); const res = await fetchWithJwt(jwt, "foo"); expect(res.status).toBe(403); }); @@ -2549,9 +2535,7 @@ describe("API", () => { it("write:tasks:foo matching body: auth passes", async () => { const seed = await seedTestEnvironment(getTestServer().prisma); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "write:tasks:foo", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["write:tasks:foo"]); const res = await post(jwt, "foo"); // Body validation / handler can fail later (404 if task is // missing, 400 for invalid body) β€” we only care that auth @@ -2562,18 +2546,14 @@ describe("API", () => { it("write:tasks:bar mismatching body: 403", async () => { const seed = await seedTestEnvironment(getTestServer().prisma); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "write:tasks:bar", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["write:tasks:bar"]); const res = await post(jwt, "foo"); expect(res.status).toBe(403); }); it("write:sessions: auth passes (was a superScope)", async () => { const seed = await seedTestEnvironment(getTestServer().prisma); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "write:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["write:sessions"]); const res = await post(jwt, "foo"); expect(res.status).not.toBe(401); expect(res.status).not.toBe(403); @@ -2597,9 +2577,7 @@ describe("API", () => { it("read:tasks:foo (wrong action): 403", async () => { const seed = await seedTestEnvironment(getTestServer().prisma); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "read:tasks:foo", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["read:tasks:foo"]); const res = await post(jwt, "foo"); expect(res.status).toBe(403); }); @@ -2649,9 +2627,7 @@ describe("API", () => { const server = getTestServer(); const seed = await seedTestEnvironment(server.prisma); const session = await seedTestApiSession(server.prisma, seed.environment); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "read:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["read:sessions"]); const res = await get(session.friendlyId, jwt); expect(res.status).toBe(200); }); @@ -2705,9 +2681,7 @@ describe("API", () => { const server = getTestServer(); const seed = await seedTestEnvironment(server.prisma); const session = await seedTestApiSession(server.prisma, seed.environment); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "admin:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["admin:sessions"]); const res = await patch(session.friendlyId, jwt); expect(res.status).toBe(200); }); @@ -2734,9 +2708,7 @@ describe("API", () => { const server = getTestServer(); const seed = await seedTestEnvironment(server.prisma); const session = await seedTestApiSession(server.prisma, seed.environment); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "write:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["write:sessions"]); const res = await patch(session.friendlyId, jwt); expect(res.status).toBe(403); }); @@ -2747,17 +2719,14 @@ describe("API", () => { // action: "admin" β€” same matrix as PATCH. describe("Close session β€” POST /api/v1/sessions/:session/close", () => { const close = async (sessionParam: string, jwt: string) => - getTestServer().webapp.fetch( - `/api/v1/sessions/${sessionParam}/close`, - { - method: "POST", - headers: { - Authorization: `Bearer ${jwt}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ reason: "test" }), - } - ); + getTestServer().webapp.fetch(`/api/v1/sessions/${sessionParam}/close`, { + method: "POST", + headers: { + Authorization: `Bearer ${jwt}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ reason: "test" }), + }); const mintJwt = async (apiKey: string, envId: string, scopes: string[]) => generateJWT({ @@ -2770,9 +2739,7 @@ describe("API", () => { const server = getTestServer(); const seed = await seedTestEnvironment(server.prisma); const session = await seedTestApiSession(server.prisma, seed.environment); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "admin:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["admin:sessions"]); const res = await close(session.friendlyId, jwt); expect(res.status).not.toBe(401); expect(res.status).not.toBe(403); @@ -2792,9 +2759,7 @@ describe("API", () => { const server = getTestServer(); const seed = await seedTestEnvironment(server.prisma); const session = await seedTestApiSession(server.prisma, seed.environment); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "write:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["write:sessions"]); const res = await close(session.friendlyId, jwt); expect(res.status).toBe(403); }); @@ -2805,22 +2770,19 @@ describe("API", () => { // action: "write" β€” multi-key sessions resource. describe("End-and-continue β€” POST /api/v1/sessions/:session/end-and-continue", () => { const endAndContinue = async (sessionParam: string, jwt: string) => - getTestServer().webapp.fetch( - `/api/v1/sessions/${sessionParam}/end-and-continue`, - { - method: "POST", - headers: { - Authorization: `Bearer ${jwt}`, - "Content-Type": "application/json", - }, - // Body shape doesn't matter for auth β€” handler runs after - // the auth check so any 4xx here means auth passed. - body: JSON.stringify({ - reason: "test", - callingRunId: "run_does_not_exist", - }), - } - ); + getTestServer().webapp.fetch(`/api/v1/sessions/${sessionParam}/end-and-continue`, { + method: "POST", + headers: { + Authorization: `Bearer ${jwt}`, + "Content-Type": "application/json", + }, + // Body shape doesn't matter for auth β€” handler runs after + // the auth check so any 4xx here means auth passed. + body: JSON.stringify({ + reason: "test", + callingRunId: "run_does_not_exist", + }), + }); const mintJwt = async (apiKey: string, envId: string, scopes: string[]) => generateJWT({ @@ -2833,9 +2795,7 @@ describe("API", () => { const server = getTestServer(); const seed = await seedTestEnvironment(server.prisma); const session = await seedTestApiSession(server.prisma, seed.environment); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "write:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["write:sessions"]); const res = await endAndContinue(session.friendlyId, jwt); expect(res.status).not.toBe(401); expect(res.status).not.toBe(403); @@ -2855,9 +2815,7 @@ describe("API", () => { const server = getTestServer(); const seed = await seedTestEnvironment(server.prisma); const session = await seedTestApiSession(server.prisma, seed.environment); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "read:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["read:sessions"]); const res = await endAndContinue(session.friendlyId, jwt); expect(res.status).toBe(403); }); @@ -2869,8 +2827,7 @@ describe("API", () => { // multi-key sessions resource. No deep matrix here; one positive // test per old superScope per method is enough. describe("Realtime IO β€” /realtime/v1/sessions/:session/:io", () => { - const ioPath = (sessionParam: string) => - `/realtime/v1/sessions/${sessionParam}/in`; + const ioPath = (sessionParam: string) => `/realtime/v1/sessions/${sessionParam}/in`; const mintJwt = async (apiKey: string, envId: string, scopes: string[]) => generateJWT({ @@ -2883,9 +2840,7 @@ describe("API", () => { const server = getTestServer(); const seed = await seedTestEnvironment(server.prisma); const session = await seedTestApiSession(server.prisma, seed.environment); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "read:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["read:sessions"]); const res = await server.webapp.fetch(ioPath(session.friendlyId), { method: "HEAD", headers: { Authorization: `Bearer ${jwt}` }, @@ -2911,9 +2866,7 @@ describe("API", () => { const server = getTestServer(); const seed = await seedTestEnvironment(server.prisma); const session = await seedTestApiSession(server.prisma, seed.environment); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "write:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["write:sessions"]); const res = await server.webapp.fetch(ioPath(session.friendlyId), { method: "PUT", headers: { Authorization: `Bearer ${jwt}` }, @@ -2941,9 +2894,7 @@ describe("API", () => { const server = getTestServer(); const seed = await seedTestEnvironment(server.prisma); const session = await seedTestApiSession(server.prisma, seed.environment); - const jwt = await mintJwt(seed.apiKey, seed.environment.id, [ - "write:sessions", - ]); + const jwt = await mintJwt(seed.apiKey, seed.environment.id, ["write:sessions"]); const res = await server.webapp.fetch(appendPath(session.friendlyId), { method: "POST", headers: { diff --git a/apps/webapp/test/auth-dashboard.e2e.full.test.ts b/apps/webapp/test/auth-dashboard.e2e.full.test.ts index 948b9c6c0c..728e9c220c 100644 --- a/apps/webapp/test/auth-dashboard.e2e.full.test.ts +++ b/apps/webapp/test/auth-dashboard.e2e.full.test.ts @@ -26,11 +26,7 @@ describe("Dashboard", () => { // already proves. If the wrapper config drifts per-route in the // future, add targeted tests for the divergent ones. describe("Admin pages β€” requireSuper gate", () => { - const adminRoutes = [ - "/admin", - "/admin/concurrency", - "/admin/back-office", - ]; + const adminRoutes = ["/admin", "/admin/concurrency", "/admin/back-office"]; for (const path of adminRoutes) { describe(`GET ${path}`, () => { diff --git a/apps/webapp/test/bufferedTriggerPayload.test.ts b/apps/webapp/test/bufferedTriggerPayload.test.ts index 6280acd4c6..e685643eb9 100644 --- a/apps/webapp/test/bufferedTriggerPayload.test.ts +++ b/apps/webapp/test/bufferedTriggerPayload.test.ts @@ -90,7 +90,7 @@ describe("buildBufferedTriggerPayload", () => { expect(buildBufferedTriggerPayload(baseInput).parentRunFriendlyId).toBeNull(); expect( buildBufferedTriggerPayload({ ...baseInput, parentRunFriendlyId: "run_parent" }) - .parentRunFriendlyId, + .parentRunFriendlyId ).toBe("run_parent"); }); }); diff --git a/apps/webapp/test/chat-snapshot-integration.test.ts b/apps/webapp/test/chat-snapshot-integration.test.ts index c2a5dcce98..c730e24b96 100644 --- a/apps/webapp/test/chat-snapshot-integration.test.ts +++ b/apps/webapp/test/chat-snapshot-integration.test.ts @@ -33,7 +33,9 @@ vi.setConfig({ testTimeout: 60_000 }); // ── Helpers ──────────────────────────────────────────────────────────── -function makeSnapshot(opts: { messages?: UIMessage[]; lastOutEventId?: string } = {}): ChatSnapshotV1 { +function makeSnapshot( + opts: { messages?: UIMessage[]; lastOutEventId?: string } = {} +): ChatSnapshotV1 { return { version: 1, savedAt: 1_700_000_000_000, @@ -109,126 +111,136 @@ describe("chat snapshot integration (MinIO + SDK helpers)", () => { expect(result).toEqual(snapshot); }); - postgresAndMinioTest("returns undefined for a fresh session with no snapshot", async ({ minioConfig }) => { - env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; - env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; - env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; - env.OBJECT_STORE_REGION = minioConfig.region; - env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; - - stubApiClient({ projectRef: "proj_snap_404", envSlug: "dev" }); - - warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - - // Session never had a snapshot written β€” read returns undefined. - const result = await readChatSnapshot("sess_never_existed"); - expect(result).toBeUndefined(); - }); - - postgresAndMinioTest("overwrites a prior snapshot in place (single-writer)", async ({ minioConfig }) => { - // The runtime guarantees one attempt alive at a time, and - // `writeChatSnapshot` runs awaited after `onTurnComplete`. Verify - // that a second write to the same key replaces the first cleanly β€” - // the read-after-write reflects the latest blob. - env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; - env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; - env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; - env.OBJECT_STORE_REGION = minioConfig.region; - env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; - - stubApiClient({ projectRef: "proj_snap_overwrite", envSlug: "dev" }); - - const sessionId = "sess_overwrite"; - - const turn1 = makeSnapshot({ - messages: [ - { id: "u-1", role: "user", parts: [{ type: "text", text: "first" }] }, - ], - lastOutEventId: "evt-turn1", - }); - const turn2 = makeSnapshot({ - messages: [ - { id: "u-1", role: "user", parts: [{ type: "text", text: "first" }] }, - { id: "a-1", role: "assistant", parts: [{ type: "text", text: "reply-1" }] }, - { id: "u-2", role: "user", parts: [{ type: "text", text: "second" }] }, - { id: "a-2", role: "assistant", parts: [{ type: "text", text: "reply-2" }] }, - ], - lastOutEventId: "evt-turn2", - }); - - await writeChatSnapshot(sessionId, turn1); - await writeChatSnapshot(sessionId, turn2); - - const result = await readChatSnapshot(sessionId); - expect(result).toEqual(turn2); - expect(result?.messages).toHaveLength(4); - expect(result?.lastOutEventId).toBe("evt-turn2"); - }); - - postgresAndMinioTest("isolates snapshots by sessionId (no cross-talk)", async ({ minioConfig }) => { - env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; - env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; - env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; - env.OBJECT_STORE_REGION = minioConfig.region; - env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; - - stubApiClient({ projectRef: "proj_snap_iso", envSlug: "dev" }); - - const sessA = "sess_iso_A"; - const sessB = "sess_iso_B"; - const snapA = makeSnapshot({ lastOutEventId: "evt-A" }); - const snapB = makeSnapshot({ lastOutEventId: "evt-B" }); - - await writeChatSnapshot(sessA, snapA); - await writeChatSnapshot(sessB, snapB); + postgresAndMinioTest( + "returns undefined for a fresh session with no snapshot", + async ({ minioConfig }) => { + env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; + env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; + env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; + env.OBJECT_STORE_REGION = minioConfig.region; + env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; - const readA = await readChatSnapshot(sessA); - const readB = await readChatSnapshot(sessB); - - expect(readA?.lastOutEventId).toBe("evt-A"); - expect(readB?.lastOutEventId).toBe("evt-B"); - // Distinct objects β€” modifying one shouldn't affect the other. - expect(readA?.lastOutEventId).not.toBe(readB?.lastOutEventId); - }); - - postgresAndMinioTest("handles snapshots with large message lists (~50 messages)", async ({ minioConfig }) => { - // Stress test: a 50-turn chat snapshot. Plan F.4 mentions the - // pre-change baseline grew past 512 KiB around turn 10-30 with tool - // use; the post-slim wire keeps wire payloads small but the snapshot - // itself can still get large. Verify the helpers handle a realistic - // payload size. - env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; - env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; - env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; - env.OBJECT_STORE_REGION = minioConfig.region; - env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; + stubApiClient({ projectRef: "proj_snap_404", envSlug: "dev" }); - stubApiClient({ projectRef: "proj_snap_big", envSlug: "dev" }); + warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - const messages: UIMessage[] = []; - for (let i = 0; i < 50; i++) { - messages.push({ - id: `u-${i}`, - role: "user", - parts: [{ type: "text", text: `user message ${i}: ${"x".repeat(200)}` }], + // Session never had a snapshot written β€” read returns undefined. + const result = await readChatSnapshot("sess_never_existed"); + expect(result).toBeUndefined(); + } + ); + + postgresAndMinioTest( + "overwrites a prior snapshot in place (single-writer)", + async ({ minioConfig }) => { + // The runtime guarantees one attempt alive at a time, and + // `writeChatSnapshot` runs awaited after `onTurnComplete`. Verify + // that a second write to the same key replaces the first cleanly β€” + // the read-after-write reflects the latest blob. + env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; + env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; + env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; + env.OBJECT_STORE_REGION = minioConfig.region; + env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; + + stubApiClient({ projectRef: "proj_snap_overwrite", envSlug: "dev" }); + + const sessionId = "sess_overwrite"; + + const turn1 = makeSnapshot({ + messages: [{ id: "u-1", role: "user", parts: [{ type: "text", text: "first" }] }], + lastOutEventId: "evt-turn1", }); - messages.push({ - id: `a-${i}`, - role: "assistant", - parts: [{ type: "text", text: `assistant reply ${i}: ${"y".repeat(500)}` }], + const turn2 = makeSnapshot({ + messages: [ + { id: "u-1", role: "user", parts: [{ type: "text", text: "first" }] }, + { id: "a-1", role: "assistant", parts: [{ type: "text", text: "reply-1" }] }, + { id: "u-2", role: "user", parts: [{ type: "text", text: "second" }] }, + { id: "a-2", role: "assistant", parts: [{ type: "text", text: "reply-2" }] }, + ], + lastOutEventId: "evt-turn2", }); + + await writeChatSnapshot(sessionId, turn1); + await writeChatSnapshot(sessionId, turn2); + + const result = await readChatSnapshot(sessionId); + expect(result).toEqual(turn2); + expect(result?.messages).toHaveLength(4); + expect(result?.lastOutEventId).toBe("evt-turn2"); } - const snapshot = makeSnapshot({ messages, lastOutEventId: "evt-50" }); - - await writeChatSnapshot("sess_big_chat", snapshot); - const result = await readChatSnapshot("sess_big_chat"); - - expect(result).toBeDefined(); - expect(result!.messages).toHaveLength(100); - expect(result!.lastOutEventId).toBe("evt-50"); - // Spot-check ordering integrity β€” the messages array round-tripped - // in the same order. - expect(result!.messages[0]!.id).toBe("u-0"); - expect(result!.messages[99]!.id).toBe("a-49"); - }); + ); + + postgresAndMinioTest( + "isolates snapshots by sessionId (no cross-talk)", + async ({ minioConfig }) => { + env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; + env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; + env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; + env.OBJECT_STORE_REGION = minioConfig.region; + env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; + + stubApiClient({ projectRef: "proj_snap_iso", envSlug: "dev" }); + + const sessA = "sess_iso_A"; + const sessB = "sess_iso_B"; + const snapA = makeSnapshot({ lastOutEventId: "evt-A" }); + const snapB = makeSnapshot({ lastOutEventId: "evt-B" }); + + await writeChatSnapshot(sessA, snapA); + await writeChatSnapshot(sessB, snapB); + + const readA = await readChatSnapshot(sessA); + const readB = await readChatSnapshot(sessB); + + expect(readA?.lastOutEventId).toBe("evt-A"); + expect(readB?.lastOutEventId).toBe("evt-B"); + // Distinct objects β€” modifying one shouldn't affect the other. + expect(readA?.lastOutEventId).not.toBe(readB?.lastOutEventId); + } + ); + + postgresAndMinioTest( + "handles snapshots with large message lists (~50 messages)", + async ({ minioConfig }) => { + // Stress test: a 50-turn chat snapshot. Plan F.4 mentions the + // pre-change baseline grew past 512 KiB around turn 10-30 with tool + // use; the post-slim wire keeps wire payloads small but the snapshot + // itself can still get large. Verify the helpers handle a realistic + // payload size. + env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl; + env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId; + env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey; + env.OBJECT_STORE_REGION = minioConfig.region; + env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined; + + stubApiClient({ projectRef: "proj_snap_big", envSlug: "dev" }); + + const messages: UIMessage[] = []; + for (let i = 0; i < 50; i++) { + messages.push({ + id: `u-${i}`, + role: "user", + parts: [{ type: "text", text: `user message ${i}: ${"x".repeat(200)}` }], + }); + messages.push({ + id: `a-${i}`, + role: "assistant", + parts: [{ type: "text", text: `assistant reply ${i}: ${"y".repeat(500)}` }], + }); + } + const snapshot = makeSnapshot({ messages, lastOutEventId: "evt-50" }); + + await writeChatSnapshot("sess_big_chat", snapshot); + const result = await readChatSnapshot("sess_big_chat"); + + expect(result).toBeDefined(); + expect(result!.messages).toHaveLength(100); + expect(result!.lastOutEventId).toBe("evt-50"); + // Spot-check ordering integrity β€” the messages array round-tripped + // in the same order. + expect(result!.messages[0]!.id).toBe("u-0"); + expect(result!.messages[99]!.id).toBe("a-49"); + } + ); }); diff --git a/apps/webapp/test/clickhouseFactory.test.ts b/apps/webapp/test/clickhouseFactory.test.ts index 7f19ea1f21..17e4502590 100644 --- a/apps/webapp/test/clickhouseFactory.test.ts +++ b/apps/webapp/test/clickhouseFactory.test.ts @@ -13,17 +13,14 @@ const TEST_URL = "https://default:password@ch-org.example.com:8443"; const TEST_URL_2 = "https://default:password@ch-other.example.com:8443"; describe("ClickHouse Factory", () => { - postgresTest( - "returns default client when org has no data store", - async ({ prisma }) => { - const registry = new OrganizationDataStoresRegistry(prisma, prisma); - await registry.loadFromDatabase(); - - const factory = new ClickhouseFactory(registry); - const client = await factory.getClickhouseForOrganization("org-no-store", "standard"); - expect(client).toBeDefined(); - } - ); + postgresTest("returns default client when org has no data store", async ({ prisma }) => { + const registry = new OrganizationDataStoresRegistry(prisma, prisma); + await registry.loadFromDatabase(); + + const factory = new ClickhouseFactory(registry); + const client = await factory.getClickhouseForOrganization("org-no-store", "standard"); + expect(client).toBeDefined(); + }); postgresTest( "returns org-specific client when a data store is configured", diff --git a/apps/webapp/test/components/code/tsql/tsqlCompletion.test.ts b/apps/webapp/test/components/code/tsql/tsqlCompletion.test.ts index f5e56fb6e2..8032bc851c 100644 --- a/apps/webapp/test/components/code/tsql/tsqlCompletion.test.ts +++ b/apps/webapp/test/components/code/tsql/tsqlCompletion.test.ts @@ -325,4 +325,3 @@ describe("createTSQLCompletion", () => { }); }); }); - diff --git a/apps/webapp/test/components/code/tsql/tsqlLinter.test.ts b/apps/webapp/test/components/code/tsql/tsqlLinter.test.ts index a669c1209d..efd0d0bfd1 100644 --- a/apps/webapp/test/components/code/tsql/tsqlLinter.test.ts +++ b/apps/webapp/test/components/code/tsql/tsqlLinter.test.ts @@ -28,9 +28,7 @@ describe("tsqlLinter", () => { true ); expect( - isValidTSQLQuery( - "SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id" - ) + isValidTSQLQuery("SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id") ).toBe(true); }); @@ -76,4 +74,3 @@ describe("tsqlLinter", () => { }); }); }); - diff --git a/apps/webapp/test/components/runs/v3/RunTag.test.ts b/apps/webapp/test/components/runs/v3/RunTag.test.ts index e2c2bec646..244695a6bf 100644 --- a/apps/webapp/test/components/runs/v3/RunTag.test.ts +++ b/apps/webapp/test/components/runs/v3/RunTag.test.ts @@ -52,17 +52,23 @@ describe("splitTag", () => { it("should handle special characters in values", () => { expect(splitTag("region:us-west-2")).toEqual({ key: "region", value: "us-west-2" }); - expect(splitTag("query:SELECT * FROM users")).toEqual({ key: "query", value: "SELECT * FROM users" }); + expect(splitTag("query:SELECT * FROM users")).toEqual({ + key: "query", + value: "SELECT * FROM users", + }); expect(splitTag("path:/api/v1/users")).toEqual({ key: "path", value: "/api/v1/users" }); }); it("should handle values containing numbers and special formats", () => { - expect(splitTag("uuid:123e4567-e89b-12d3-a456-426614174000")).toEqual({ - key: "uuid", - value: "123e4567-e89b-12d3-a456-426614174000" + expect(splitTag("uuid:123e4567-e89b-12d3-a456-426614174000")).toEqual({ + key: "uuid", + value: "123e4567-e89b-12d3-a456-426614174000", }); expect(splitTag("ip_192.168.1.1")).toEqual({ key: "ip", value: "192.168.1.1" }); - expect(splitTag("date:2023-04-01T12:00:00Z")).toEqual({ key: "date", value: "2023-04-01T12:00:00Z" }); + expect(splitTag("date:2023-04-01T12:00:00Z")).toEqual({ + key: "date", + value: "2023-04-01T12:00:00Z", + }); }); it("should handle keys with numbers", () => { @@ -71,7 +77,13 @@ describe("splitTag", () => { }); it("should handle particularly complex mixed cases", () => { - expect(splitTag("env:prod_us-west-2_replica")).toEqual({ key: "env", value: "prod_us-west-2_replica" }); - expect(splitTag("status_error:connection:timeout")).toEqual({ key: "status", value: "error:connection:timeout" }); + expect(splitTag("env:prod_us-west-2_replica")).toEqual({ + key: "env", + value: "prod_us-west-2_replica", + }); + expect(splitTag("status_error:connection:timeout")).toEqual({ + key: "status", + value: "error:connection:timeout", + }); }); -}); \ No newline at end of file +}); diff --git a/apps/webapp/test/computeMigration.test.ts b/apps/webapp/test/computeMigration.test.ts index 0c914f2e2a..b828409cd3 100644 --- a/apps/webapp/test/computeMigration.test.ts +++ b/apps/webapp/test/computeMigration.test.ts @@ -16,12 +16,20 @@ describe("isOrgMigrated", () => { }); it("does not migrate when globally disabled", () => { expect( - isOrgMigrated({ ...base, orgId: "org_x", flags: { computeMigrationEnabled: false, computeMigrationFreePercentage: 100 } }) + isOrgMigrated({ + ...base, + orgId: "org_x", + flags: { computeMigrationEnabled: false, computeMigrationFreePercentage: 100 }, + }) ).toBe(false); }); it("per-org override false excludes even at 100%", () => { expect( - isOrgMigrated({ ...base, orgId: "org_x", orgFeatureFlags: { computeMigrationEnabled: false } }) + isOrgMigrated({ + ...base, + orgId: "org_x", + orgFeatureFlags: { computeMigrationEnabled: false }, + }) ).toBe(false); }); it("per-org override true enrolls even when globally off", () => { @@ -50,13 +58,22 @@ describe("isOrgMigrated", () => { planType: "enterprise", orgId: "org_x", orgFeatureFlags: {}, - flags: { computeMigrationEnabled: true, computeMigrationFreePercentage: 100, computeMigrationPaidPercentage: 100 }, + flags: { + computeMigrationEnabled: true, + computeMigrationFreePercentage: 100, + computeMigrationPaidPercentage: 100, + }, }) ).toBe(false); }); it("undefined planType is not enrolled", () => { expect( - isOrgMigrated({ planType: undefined, orgId: "org_x", orgFeatureFlags: {}, flags: { computeMigrationEnabled: true } }) + isOrgMigrated({ + planType: undefined, + orgId: "org_x", + orgFeatureFlags: {}, + flags: { computeMigrationEnabled: true }, + }) ).toBe(false); }); }); @@ -74,7 +91,12 @@ describe("resolveComputeMigration", () => { it("swaps to the compute backing for an enrolled free org", () => { expect( - resolveComputeMigration({ ...enrolled, baseWorkerQueue: "us-east-1", orgId: "org_x", backing }) + resolveComputeMigration({ + ...enrolled, + baseWorkerQueue: "us-east-1", + orgId: "org_x", + backing, + }) ).toEqual({ workerQueue: "us-east-1-next", region: "us-east-1", enableFastPath: true }); }); it("leaves the queue unchanged when there is no backing for the region (EU)", () => { @@ -90,17 +112,35 @@ describe("resolveComputeMigration", () => { }); it("does not migrate DEVELOPMENT", () => { expect( - resolveComputeMigration({ ...enrolled, baseWorkerQueue: "us-east-1", orgId: "org_x", backing, envType: "DEVELOPMENT" }) + resolveComputeMigration({ + ...enrolled, + baseWorkerQueue: "us-east-1", + orgId: "org_x", + backing, + envType: "DEVELOPMENT", + }) ).toEqual({ workerQueue: "us-east-1", region: "us-east-1", enableFastPath: false }); }); it("leaves a non-enrolled org untouched", () => { expect( - resolveComputeMigration({ ...enrolled, baseWorkerQueue: "us-east-1", orgId: "org_x", backing, flags: { computeMigrationEnabled: false } }) + resolveComputeMigration({ + ...enrolled, + baseWorkerQueue: "us-east-1", + orgId: "org_x", + backing, + flags: { computeMigrationEnabled: false }, + }) ).toEqual({ workerQueue: "us-east-1", region: "us-east-1", enableFastPath: false }); }); it("undefined baseWorkerQueue passes through", () => { expect( - resolveComputeMigration({ ...enrolled, baseWorkerQueue: undefined, region: undefined, orgId: "org_x", backing }) + resolveComputeMigration({ + ...enrolled, + baseWorkerQueue: undefined, + region: undefined, + orgId: "org_x", + backing, + }) ).toEqual({ workerQueue: undefined, region: undefined, enableFastPath: false }); }); }); diff --git a/apps/webapp/test/createDeploymentWithNextVersion.test.ts b/apps/webapp/test/createDeploymentWithNextVersion.test.ts index aa135b3a31..7b4fda2b82 100644 --- a/apps/webapp/test/createDeploymentWithNextVersion.test.ts +++ b/apps/webapp/test/createDeploymentWithNextVersion.test.ts @@ -67,24 +67,21 @@ describe("createDeploymentWithNextVersion", () => { } ); - containerTest( - "propagates non-P2002 errors immediately without retrying", - async ({ prisma }) => { - const { environment } = await seedEnvironment(prisma); + containerTest("propagates non-P2002 errors immediately without retrying", async ({ prisma }) => { + const { environment } = await seedEnvironment(prisma); - let buildDataCalls = 0; - const buildData = () => { - buildDataCalls++; - throw new Error("builder boom"); - }; + let buildDataCalls = 0; + const buildData = () => { + buildDataCalls++; + throw new Error("builder boom"); + }; - await expect( - createDeploymentWithNextVersion(prisma, environment.id, buildData) - ).rejects.toThrow("builder boom"); + await expect( + createDeploymentWithNextVersion(prisma, environment.id, buildData) + ).rejects.toThrow("builder boom"); - expect(buildDataCalls).toBe(1); - } - ); + expect(buildDataCalls).toBe(1); + }); containerTest( "wraps exhausted retries in DeploymentVersionCollisionError with the P2002 as cause", @@ -113,9 +110,7 @@ describe("createDeploymentWithNextVersion", () => { ); const fulfilled = settled.filter((s) => s.status === "fulfilled"); - const rejected = settled.filter( - (s): s is PromiseRejectedResult => s.status === "rejected" - ); + const rejected = settled.filter((s): s is PromiseRejectedResult => s.status === "rejected"); expect(fulfilled.length).toBeGreaterThanOrEqual(1); expect(rejected.length).toBeGreaterThanOrEqual(1); diff --git a/apps/webapp/test/devBranchServices.test.ts b/apps/webapp/test/devBranchServices.test.ts index f8d333f5ad..06c06f7c7e 100644 --- a/apps/webapp/test/devBranchServices.test.ts +++ b/apps/webapp/test/devBranchServices.test.ts @@ -30,93 +30,109 @@ async function createDevRoot( } describe("UpsertBranchService β€” DEVELOPMENT parent", () => { - postgresTest("creates a child branch that inherits the parent's ownership", async ({ prisma }) => { - const { organization, project, user, orgMember } = await createTestOrgProjectWithMember(prisma); - const devRoot = await createDevRoot(prisma, project.id, organization.id, orgMember.id); - - const result = await new UpsertBranchService(prisma).call( - { type: "userMembership", userId: user.id }, - { projectId: project.id, env: "development", branchName: "my-feature" } - ); - - expect(result.success).toBe(true); - if (!result.success) return; - - const { branch } = result; - expect(branch.type).toBe("DEVELOPMENT"); - expect(branch.parentEnvironmentId).toBe(devRoot.id); - expect(branch.branchName).toBe("my-feature"); - // The key dev-vs-preview divergence: dev branches MUST copy the parent's - // orgMemberId (preview parents have none). - expect(branch.orgMemberId).toBe(orgMember.id); - // Children inherit the parent's concurrency limit at creation. - expect(branch.maximumConcurrencyLimit).toBe(17); - expect(branch.slug).toBe(slug(`${devRoot.slug}-my-feature`)); - }); - - postgresTest("is idempotent β€” upserting the same branch returns the existing row", async ({ prisma }) => { - const { organization, project, user, orgMember } = await createTestOrgProjectWithMember(prisma); - await createDevRoot(prisma, project.id, organization.id, orgMember.id); - const orgFilter = { type: "userMembership" as const, userId: user.id }; - const options = { projectId: project.id, env: "development" as const, branchName: "dup" }; - - const first = await new UpsertBranchService(prisma).call(orgFilter, options); - const second = await new UpsertBranchService(prisma).call(orgFilter, options); - - expect(first.success && second.success).toBe(true); - if (!first.success || !second.success) return; - expect(second.alreadyExisted).toBe(true); - expect(second.branch.id).toBe(first.branch.id); - }); - - postgresTest("rejects an invalid branch name without touching the database", async ({ prisma }) => { - const { organization, project, user, orgMember } = await createTestOrgProjectWithMember(prisma); - await createDevRoot(prisma, project.id, organization.id, orgMember.id); - - const result = await new UpsertBranchService(prisma).call( - { type: "userMembership", userId: user.id }, - { projectId: project.id, env: "development", branchName: "bad branch name!" } - ); - - expect(result.success).toBe(false); - }); + postgresTest( + "creates a child branch that inherits the parent's ownership", + async ({ prisma }) => { + const { organization, project, user, orgMember } = + await createTestOrgProjectWithMember(prisma); + const devRoot = await createDevRoot(prisma, project.id, organization.id, orgMember.id); + + const result = await new UpsertBranchService(prisma).call( + { type: "userMembership", userId: user.id }, + { projectId: project.id, env: "development", branchName: "my-feature" } + ); + + expect(result.success).toBe(true); + if (!result.success) return; + + const { branch } = result; + expect(branch.type).toBe("DEVELOPMENT"); + expect(branch.parentEnvironmentId).toBe(devRoot.id); + expect(branch.branchName).toBe("my-feature"); + // The key dev-vs-preview divergence: dev branches MUST copy the parent's + // orgMemberId (preview parents have none). + expect(branch.orgMemberId).toBe(orgMember.id); + // Children inherit the parent's concurrency limit at creation. + expect(branch.maximumConcurrencyLimit).toBe(17); + expect(branch.slug).toBe(slug(`${devRoot.slug}-my-feature`)); + } + ); + + postgresTest( + "is idempotent β€” upserting the same branch returns the existing row", + async ({ prisma }) => { + const { organization, project, user, orgMember } = + await createTestOrgProjectWithMember(prisma); + await createDevRoot(prisma, project.id, organization.id, orgMember.id); + const orgFilter = { type: "userMembership" as const, userId: user.id }; + const options = { projectId: project.id, env: "development" as const, branchName: "dup" }; + + const first = await new UpsertBranchService(prisma).call(orgFilter, options); + const second = await new UpsertBranchService(prisma).call(orgFilter, options); + + expect(first.success && second.success).toBe(true); + if (!first.success || !second.success) return; + expect(second.alreadyExisted).toBe(true); + expect(second.branch.id).toBe(first.branch.id); + } + ); + + postgresTest( + "rejects an invalid branch name without touching the database", + async ({ prisma }) => { + const { organization, project, user, orgMember } = + await createTestOrgProjectWithMember(prisma); + await createDevRoot(prisma, project.id, organization.id, orgMember.id); + + const result = await new UpsertBranchService(prisma).call( + { type: "userMembership", userId: user.id }, + { projectId: project.id, env: "development", branchName: "bad branch name!" } + ); + + expect(result.success).toBe(false); + } + ); }); describe("ArchiveBranchService β€” DEVELOPMENT", () => { - postgresTest("archives a dev branch and frees its slug/shortcode for reuse", async ({ prisma }) => { - const { organization, project, user, orgMember } = await createTestOrgProjectWithMember(prisma); - await createDevRoot(prisma, project.id, organization.id, orgMember.id); - const orgFilter = { type: "userMembership" as const, userId: user.id }; - - const created = await new UpsertBranchService(prisma).call(orgFilter, { - projectId: project.id, - env: "development", - branchName: "reuse-me", - }); - expect(created.success).toBe(true); - if (!created.success) return; - const originalSlug = created.branch.slug; - - const archived = await new ArchiveBranchService(prisma).call(orgFilter, { - environmentId: created.branch.id, - }); - expect(archived.success).toBe(true); - if (!archived.success) return; - expect(archived.branch.archivedAt).not.toBeNull(); - // Slug + shortcode are randomized on archive so the name can be reused. - expect(archived.branch.slug).not.toBe(originalSlug); - - // The same branch name can now be created again (new row, deterministic slug). - const recreated = await new UpsertBranchService(prisma).call(orgFilter, { - projectId: project.id, - env: "development", - branchName: "reuse-me", - }); - expect(recreated.success).toBe(true); - if (!recreated.success) return; - expect(recreated.branch.id).not.toBe(created.branch.id); - expect(recreated.branch.slug).toBe(originalSlug); - }); + postgresTest( + "archives a dev branch and frees its slug/shortcode for reuse", + async ({ prisma }) => { + const { organization, project, user, orgMember } = + await createTestOrgProjectWithMember(prisma); + await createDevRoot(prisma, project.id, organization.id, orgMember.id); + const orgFilter = { type: "userMembership" as const, userId: user.id }; + + const created = await new UpsertBranchService(prisma).call(orgFilter, { + projectId: project.id, + env: "development", + branchName: "reuse-me", + }); + expect(created.success).toBe(true); + if (!created.success) return; + const originalSlug = created.branch.slug; + + const archived = await new ArchiveBranchService(prisma).call(orgFilter, { + environmentId: created.branch.id, + }); + expect(archived.success).toBe(true); + if (!archived.success) return; + expect(archived.branch.archivedAt).not.toBeNull(); + // Slug + shortcode are randomized on archive so the name can be reused. + expect(archived.branch.slug).not.toBe(originalSlug); + + // The same branch name can now be created again (new row, deterministic slug). + const recreated = await new UpsertBranchService(prisma).call(orgFilter, { + projectId: project.id, + env: "development", + branchName: "reuse-me", + }); + expect(recreated.success).toBe(true); + if (!recreated.success) return; + expect(recreated.branch.id).not.toBe(created.branch.id); + expect(recreated.branch.slug).toBe(originalSlug); + } + ); postgresTest("refuses to archive the default branch (the dev root)", async ({ prisma }) => { const { organization, project, user, orgMember } = await createTestOrgProjectWithMember(prisma); diff --git a/apps/webapp/test/devPresenceRecency.test.ts b/apps/webapp/test/devPresenceRecency.test.ts index aafeb8bd70..6aac3c936b 100644 --- a/apps/webapp/test/devPresenceRecency.test.ts +++ b/apps/webapp/test/devPresenceRecency.test.ts @@ -14,13 +14,16 @@ function ids() { const recentKey = (userId: string, projectId: string) => `dev-recent:${userId}:${projectId}`; describe("DevPresence β€” recency ZSET", () => { - redisTest("getRecentBranchIds returns an empty map when nothing has pinged", async ({ redisOptions }) => { - const presence = new DevPresence(redisOptions); - const { userId, projectId } = ids(); - - const result = await presence.getRecentBranchIds(userId, projectId); - expect(result.size).toBe(0); - }); + redisTest( + "getRecentBranchIds returns an empty map when nothing has pinged", + async ({ redisOptions }) => { + const presence = new DevPresence(redisOptions); + const { userId, projectId } = ids(); + + const result = await presence.getRecentBranchIds(userId, projectId); + expect(result.size).toBe(0); + } + ); redisTest("a ping records the branch as recently active", async ({ redisOptions }) => { const presence = new DevPresence(redisOptions); @@ -87,21 +90,24 @@ describe("DevPresence β€” recency ZSET", () => { await redis.quit(); }); - redisTest("caps cardinality at 50 even under a flood of distinct branches", async ({ redisOptions }) => { - const presence = new DevPresence(redisOptions); - const redis = new Redis(redisOptions); - const { userId, projectId } = ids(); + redisTest( + "caps cardinality at 50 even under a flood of distinct branches", + async ({ redisOptions }) => { + const presence = new DevPresence(redisOptions); + const redis = new Redis(redisOptions); + const { userId, projectId } = ids(); - // 60 distinct envs, each with its own debounce key, so each performs a ZADD. - for (let i = 0; i < 60; i++) { - // eslint-disable-next-line no-await-in-loop - await presence.setConnected({ userId, projectId, environmentId: `env_${i}`, ttl: 60 }); - } + // 60 distinct envs, each with its own debounce key, so each performs a ZADD. + for (let i = 0; i < 60; i++) { + // eslint-disable-next-line no-await-in-loop + await presence.setConnected({ userId, projectId, environmentId: `env_${i}`, ttl: 60 }); + } - expect(await redis.zcard(recentKey(userId, projectId))).toBe(50); + expect(await redis.zcard(recentKey(userId, projectId))).toBe(50); - await redis.quit(); - }); + await redis.quit(); + } + ); redisTest("returns recent branches in most-recent-first order", async ({ redisOptions }) => { const presence = new DevPresence(redisOptions); diff --git a/apps/webapp/test/duplicateTaskIds.test.ts b/apps/webapp/test/duplicateTaskIds.test.ts index f0f95806c5..f5e722e39b 100644 --- a/apps/webapp/test/duplicateTaskIds.test.ts +++ b/apps/webapp/test/duplicateTaskIds.test.ts @@ -2,7 +2,12 @@ import { describe, it, expect } from "vitest"; import { ServiceValidationError } from "~/v3/services/common.server"; import { assertNoDuplicateTaskIds } from "~/v3/services/duplicateTaskIds.server"; -function task(partial: { id: string; filePath?: string; exportName?: string; triggerSource?: string }) { +function task(partial: { + id: string; + filePath?: string; + exportName?: string; + triggerSource?: string; +}) { return { filePath: "src/trigger/example.ts", exportName: "exampleTask", diff --git a/apps/webapp/test/engine/streamBatchItems.test.ts b/apps/webapp/test/engine/streamBatchItems.test.ts index c043a4e9e5..70dba77661 100644 --- a/apps/webapp/test/engine/streamBatchItems.test.ts +++ b/apps/webapp/test/engine/streamBatchItems.test.ts @@ -1790,17 +1790,17 @@ describe("StreamBatchItemsService", () => { `\n[streamBatchItems perf] runCount=${runCount}\n` + ` large payloads (${offloadLatencyMs}ms/item offload):\n` + ` concurrency=1 ${offloadSeqMs.toFixed(0)}ms\n` + - ` concurrency=10 ${offload10Ms.toFixed(0)}ms (${( - offloadSeqMs / offload10Ms - ).toFixed(1)}x faster)\n` + - ` concurrency=50 ${offload50Ms.toFixed(0)}ms (${( - offloadSeqMs / offload50Ms - ).toFixed(1)}x faster)\n` + + ` concurrency=10 ${offload10Ms.toFixed(0)}ms (${(offloadSeqMs / offload10Ms).toFixed( + 1 + )}x faster)\n` + + ` concurrency=50 ${offload50Ms.toFixed(0)}ms (${(offloadSeqMs / offload50Ms).toFixed( + 1 + )}x faster)\n` + ` small payloads (Redis enqueue only, no offload):\n` + ` concurrency=1 ${enqueueSeqMs.toFixed(0)}ms\n` + - ` concurrency=10 ${enqueue10Ms.toFixed(0)}ms (${( - enqueueSeqMs / enqueue10Ms - ).toFixed(1)}x faster)\n` + ` concurrency=10 ${enqueue10Ms.toFixed(0)}ms (${(enqueueSeqMs / enqueue10Ms).toFixed( + 1 + )}x faster)\n` ); // Sequential floor: N items each held for offloadLatencyMs cannot finish diff --git a/apps/webapp/test/engine/triggerTask.test.ts b/apps/webapp/test/engine/triggerTask.test.ts index c524d0a3b9..ecd4db5e2b 100644 --- a/apps/webapp/test/engine/triggerTask.test.ts +++ b/apps/webapp/test/engine/triggerTask.test.ts @@ -23,10 +23,7 @@ import { TaskRun } from "@trigger.dev/database"; import { Redis } from "ioredis"; import { IdempotencyKeyConcern } from "~/runEngine/concerns/idempotencyKeys.server"; import { DefaultQueueManager } from "~/runEngine/concerns/queues.server"; -import { - NoopTaskMetadataCache, - RedisTaskMetadataCache, -} from "~/services/taskMetadataCache.server"; +import { NoopTaskMetadataCache, RedisTaskMetadataCache } from "~/services/taskMetadataCache.server"; import { EntitlementValidationParams, MaxAttemptsValidationParams, @@ -97,9 +94,9 @@ class MockTraceEventConcern implements TraceEventConcern { spanId: MOCK_SPAN_ID, traceContext: { traceparent: MOCK_TRACEPARENT }, traceparent: undefined, - setAttribute: () => { }, - failWithError: () => { }, - stop: () => { }, + setAttribute: () => {}, + failWithError: () => {}, + stop: () => {}, }, "test" ); @@ -122,9 +119,9 @@ class MockTraceEventConcern implements TraceEventConcern { spanId: "test", traceContext: {}, traceparent: undefined, - setAttribute: () => { }, - failWithError: () => { }, - stop: () => { }, + setAttribute: () => {}, + failWithError: () => {}, + stop: () => {}, }, "test" ); @@ -147,9 +144,9 @@ class MockTraceEventConcern implements TraceEventConcern { spanId: "test", traceContext: {}, traceparent: undefined, - setAttribute: () => { }, - failWithError: () => { }, - stop: () => { }, + setAttribute: () => {}, + failWithError: () => {}, + stop: () => {}, }, "test" ); @@ -1425,83 +1422,80 @@ describe("RunEngineTriggerTaskService", () => { } ); - containerTest( - "should accept valid debounce.delay formats", - async ({ prisma, redisOptions }) => { - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - redis: redisOptions, - }, - runLock: { - redis: redisOptions, - }, + containerTest("should accept valid debounce.delay formats", async ({ prisma, redisOptions }) => { + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, }, - baseCostInCents: 0.0005, }, - tracer: trace.getTracer("test", "0.0.0"), - }); + baseCostInCents: 0.0005, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - const taskIdentifier = "test-task"; + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - const queuesManager = new DefaultQueueManager(prisma, engine); - const idempotencyKeyConcern = new IdempotencyKeyConcern( - prisma, - engine, - new MockTraceEventConcern() - ); + const queuesManager = new DefaultQueueManager(prisma, engine); + const idempotencyKeyConcern = new IdempotencyKeyConcern( + prisma, + engine, + new MockTraceEventConcern() + ); - const triggerTaskService = new RunEngineTriggerTaskService({ - engine, - prisma, - payloadProcessor: new MockPayloadProcessor(), - queueConcern: queuesManager, - idempotencyKeyConcern, - validator: new MockTriggerTaskValidator(), - traceEventConcern: new MockTraceEventConcern(), - tracer: trace.getTracer("test", "0.0.0"), - metadataMaximumSize: 1024 * 1024 * 1, - }); + const triggerTaskService = new RunEngineTriggerTaskService({ + engine, + prisma, + payloadProcessor: new MockPayloadProcessor(), + queueConcern: queuesManager, + idempotencyKeyConcern, + validator: new MockTriggerTaskValidator(), + traceEventConcern: new MockTraceEventConcern(), + tracer: trace.getTracer("test", "0.0.0"), + metadataMaximumSize: 1024 * 1024 * 1, + }); - // Valid debounce.delay format - const result = await triggerTaskService.call({ - taskId: taskIdentifier, - environment: authenticatedEnvironment, - body: { - payload: { test: "test" }, - options: { - debounce: { - key: "test-key", - delay: "5s", // Valid format - }, + // Valid debounce.delay format + const result = await triggerTaskService.call({ + taskId: taskIdentifier, + environment: authenticatedEnvironment, + body: { + payload: { test: "test" }, + options: { + debounce: { + key: "test-key", + delay: "5s", // Valid format }, }, - }); + }, + }); - expect(result).toBeDefined(); - expect(result?.run.friendlyId).toBeDefined(); + expect(result).toBeDefined(); + expect(result?.run.friendlyId).toBeDefined(); - await engine.quit(); - } - ); + await engine.quit(); + }); // ─── Mollifier integration ────────────────────────────────────────────────── // @@ -1517,14 +1511,26 @@ describe("RunEngineTriggerTaskService", () => { this.accepted.push(input); return true; } - async pop() { return null; } + async pop() { + return null; + } async ack() {} async requeue() {} - async fail() { return false; } - async getEntry() { return null; } - async listEnvs(): Promise { return []; } - async getEntryTtlSeconds(): Promise { return -1; } - async evaluateTrip() { return { tripped: false, count: 0 }; } + async fail() { + return false; + } + async getEntry() { + return null; + } + async listEnvs(): Promise { + return []; + } + async getEntryTtlSeconds(): Promise { + return -1; + } + async evaluateTrip() { + return { tripped: false, count: 0 }; + } async close() {} } @@ -1538,7 +1544,9 @@ describe("RunEngineTriggerTaskService", () => { runLock: { redis: redisOptions }, machines: { defaultMachine: "small-1x", - machines: { "small-1x": { name: "small-1x" as const, cpu: 0.5, memory: 0.5, centsPerMs: 0.0001 } }, + machines: { + "small-1x": { name: "small-1x" as const, cpu: 0.5, memory: 0.5, centsPerMs: 0.0001 }, + }, baseCostInCents: 0.0005, }, tracer: trace.getTracer("test", "0.0.0"), @@ -1558,16 +1566,28 @@ describe("RunEngineTriggerTaskService", () => { } const buffer = new CapturingMollifierBuffer(); - const evaluateGateSpy = vi.fn(async () => ({ action: "mollify" as const, decision: { - divert: true as const, reason: "per_env_rate" as const, count: 99, threshold: 1, windowMs: 200, holdMs: 500, - } })); + const evaluateGateSpy = vi.fn(async () => ({ + action: "mollify" as const, + decision: { + divert: true as const, + reason: "per_env_rate" as const, + count: 99, + threshold: 1, + windowMs: 200, + holdMs: 500, + }, + })); const triggerTaskService = new RunEngineTriggerTaskService({ engine, prisma, payloadProcessor: new MockPayloadProcessor(), queueConcern: new DefaultQueueManager(prisma, engine), - idempotencyKeyConcern: new IdempotencyKeyConcern(prisma, engine, new MockTraceEventConcern()), + idempotencyKeyConcern: new IdempotencyKeyConcern( + prisma, + engine, + new MockTraceEventConcern() + ), validator: new FailingMaxAttemptsValidator(), traceEventConcern: new MockTraceEventConcern(), tracer: trace.getTracer("test", "0.0.0"), @@ -1582,7 +1602,7 @@ describe("RunEngineTriggerTaskService", () => { taskId: taskIdentifier, environment: authenticatedEnvironment, body: { payload: { test: "x" } }, - }), + }) ).rejects.toThrow(/synthetic max-attempts failure/); // Critical: the gate must NEVER be consulted when validation fails. @@ -1593,7 +1613,7 @@ describe("RunEngineTriggerTaskService", () => { expect(buffer.accepted).toHaveLength(0); await engine.quit(); - }, + } ); containerTest( @@ -1615,7 +1635,9 @@ describe("RunEngineTriggerTaskService", () => { runLock: { redis: redisOptions }, machines: { defaultMachine: "small-1x", - machines: { "small-1x": { name: "small-1x" as const, cpu: 0.5, memory: 0.5, centsPerMs: 0.0001 } }, + machines: { + "small-1x": { name: "small-1x" as const, cpu: 0.5, memory: 0.5, centsPerMs: 0.0001 }, + }, baseCostInCents: 0.0005, }, tracer: trace.getTracer("test", "0.0.0"), @@ -1658,7 +1680,11 @@ describe("RunEngineTriggerTaskService", () => { prisma, payloadProcessor: new MockPayloadProcessor(), queueConcern: new DefaultQueueManager(prisma, engine), - idempotencyKeyConcern: new IdempotencyKeyConcern(prisma, engine, new MockTraceEventConcern()), + idempotencyKeyConcern: new IdempotencyKeyConcern( + prisma, + engine, + new MockTraceEventConcern() + ), validator: new MockTriggerTaskValidator(), traceEventConcern: traceConcern, tracer: trace.getTracer("test", "0.0.0"), @@ -1763,7 +1789,7 @@ describe("RunEngineTriggerTaskService", () => { expect(pgRun).toBeNull(); await engine.quit(); - }, + } ); containerTest( @@ -1776,7 +1802,9 @@ describe("RunEngineTriggerTaskService", () => { runLock: { redis: redisOptions }, machines: { defaultMachine: "small-1x", - machines: { "small-1x": { name: "small-1x" as const, cpu: 0.5, memory: 0.5, centsPerMs: 0.0001 } }, + machines: { + "small-1x": { name: "small-1x" as const, cpu: 0.5, memory: 0.5, centsPerMs: 0.0001 }, + }, baseCostInCents: 0.0005, }, tracer: trace.getTracer("test", "0.0.0"), @@ -1794,7 +1822,11 @@ describe("RunEngineTriggerTaskService", () => { prisma, payloadProcessor: new MockPayloadProcessor(), queueConcern: new DefaultQueueManager(prisma, engine), - idempotencyKeyConcern: new IdempotencyKeyConcern(prisma, engine, new MockTraceEventConcern()), + idempotencyKeyConcern: new IdempotencyKeyConcern( + prisma, + engine, + new MockTraceEventConcern() + ), validator: new MockTriggerTaskValidator(), traceEventConcern: new MockTraceEventConcern(), tracer: trace.getTracer("test", "0.0.0"), @@ -1824,7 +1856,7 @@ describe("RunEngineTriggerTaskService", () => { expect(result?.isMollified).toBeFalsy(); await engine.quit(); - }, + } ); containerTest( @@ -1850,7 +1882,9 @@ describe("RunEngineTriggerTaskService", () => { runLock: { redis: redisOptions }, machines: { defaultMachine: "small-1x", - machines: { "small-1x": { name: "small-1x" as const, cpu: 0.5, memory: 0.5, centsPerMs: 0.0001 } }, + machines: { + "small-1x": { name: "small-1x" as const, cpu: 0.5, memory: 0.5, centsPerMs: 0.0001 }, + }, baseCostInCents: 0.0005, }, tracer: trace.getTracer("test", "0.0.0"), @@ -1863,7 +1897,7 @@ describe("RunEngineTriggerTaskService", () => { const idempotencyKeyConcern = new IdempotencyKeyConcern( prisma, engine, - new MockTraceEventConcern(), + new MockTraceEventConcern() ); // Setup: normal trigger to create the cached run (no mollifier). @@ -1931,9 +1965,8 @@ describe("RunEngineTriggerTaskService", () => { expect(buffer.accepted).toHaveLength(0); await engine.quit(); - }, + } ); - }); describe("DefaultQueueManager task metadata cache", () => { @@ -1981,7 +2014,11 @@ describe("DefaultQueueManager task metadata cache", () => { prisma, payloadProcessor: new MockPayloadProcessor(), queueConcern: queuesManager, - idempotencyKeyConcern: new IdempotencyKeyConcern(prisma, engine, new MockTraceEventConcern()), + idempotencyKeyConcern: new IdempotencyKeyConcern( + prisma, + engine, + new MockTraceEventConcern() + ), validator: new MockTriggerTaskValidator(), traceEventConcern: new MockTraceEventConcern(), tracer: trace.getTracer("test", "0.0.0"), @@ -2037,7 +2074,11 @@ describe("DefaultQueueManager task metadata cache", () => { prisma, payloadProcessor: new MockPayloadProcessor(), queueConcern: queuesManager, - idempotencyKeyConcern: new IdempotencyKeyConcern(prisma, engine, new MockTraceEventConcern()), + idempotencyKeyConcern: new IdempotencyKeyConcern( + prisma, + engine, + new MockTraceEventConcern() + ), validator: new MockTriggerTaskValidator(), traceEventConcern: new MockTraceEventConcern(), tracer: trace.getTracer("test", "0.0.0"), @@ -2111,7 +2152,11 @@ describe("DefaultQueueManager task metadata cache", () => { prisma, payloadProcessor: new MockPayloadProcessor(), queueConcern: queuesManager, - idempotencyKeyConcern: new IdempotencyKeyConcern(prisma, engine, new MockTraceEventConcern()), + idempotencyKeyConcern: new IdempotencyKeyConcern( + prisma, + engine, + new MockTraceEventConcern() + ), validator: new MockTriggerTaskValidator(), traceEventConcern: new MockTraceEventConcern(), tracer: trace.getTracer("test", "0.0.0"), @@ -2194,7 +2239,11 @@ describe("DefaultQueueManager task metadata cache", () => { prisma, payloadProcessor: new MockPayloadProcessor(), queueConcern: queuesManager, - idempotencyKeyConcern: new IdempotencyKeyConcern(prisma, engine, new MockTraceEventConcern()), + idempotencyKeyConcern: new IdempotencyKeyConcern( + prisma, + engine, + new MockTraceEventConcern() + ), validator: new MockTriggerTaskValidator(), traceEventConcern: new MockTraceEventConcern(), tracer: trace.getTracer("test", "0.0.0"), diff --git a/apps/webapp/test/environmentSort.test.ts b/apps/webapp/test/environmentSort.test.ts index 5efed1235d..e47912fab0 100644 --- a/apps/webapp/test/environmentSort.test.ts +++ b/apps/webapp/test/environmentSort.test.ts @@ -15,12 +15,7 @@ describe("sortEnvironments", () => { { type: "STAGING" }, ]); - expect(sorted.map((e) => e.type)).toEqual([ - "DEVELOPMENT", - "STAGING", - "PREVIEW", - "PRODUCTION", - ]); + expect(sorted.map((e) => e.type)).toEqual(["DEVELOPMENT", "STAGING", "PREVIEW", "PRODUCTION"]); }); it("sorts same-type rows by lastActivity desc when both have it", () => { @@ -95,10 +90,7 @@ describe("filterOrphanedEnvironments", () => { { type: "PRODUCTION" } as any, ]); - expect(result).toEqual([ - { type: "DEVELOPMENT", orgMemberId: "om_1" }, - { type: "PRODUCTION" }, - ]); + expect(result).toEqual([{ type: "DEVELOPMENT", orgMemberId: "om_1" }, { type: "PRODUCTION" }]); }); it("keeps DEVELOPMENT envs whose orgMember relation is loaded", () => { @@ -121,9 +113,6 @@ describe("onlyDevEnvironments / exceptDevEnvironments", () => { it("partitions on the development type", () => { expect(onlyDevEnvironments([...envs])).toEqual([{ type: "DEVELOPMENT" }]); - expect(exceptDevEnvironments([...envs])).toEqual([ - { type: "PREVIEW" }, - { type: "PRODUCTION" }, - ]); + expect(exceptDevEnvironments([...envs])).toEqual([{ type: "PREVIEW" }, { type: "PRODUCTION" }]); }); }); diff --git a/apps/webapp/test/environmentVariablesEnvironments.test.ts b/apps/webapp/test/environmentVariablesEnvironments.test.ts index 2555bcae0f..9a21506f7b 100644 --- a/apps/webapp/test/environmentVariablesEnvironments.test.ts +++ b/apps/webapp/test/environmentVariablesEnvironments.test.ts @@ -11,9 +11,7 @@ vi.mock("~/db.server", () => ({ fnOrOptions?: ((tx: unknown) => Promise) | unknown ) => { const fn = - typeof nameOrFn === "string" - ? (fnOrOptions as (tx: unknown) => Promise) - : nameOrFn; + typeof nameOrFn === "string" ? (fnOrOptions as (tx: unknown) => Promise) : nameOrFn; return prismaClient.$transaction(fn); }, @@ -45,7 +43,9 @@ describe("loadEnvironmentVariablesEnvironments", () => { }); expect(result.environments.map((environment) => environment.id)).toContain(production.id); - expect(result.environments.every((environment) => typeof environment.id === "string")).toBe(true); + expect(result.environments.every((environment) => typeof environment.id === "string")).toBe( + true + ); }); postgresTest("rejects users who are not project members", async ({ prisma }) => { @@ -125,20 +125,23 @@ describe("loadEnvironmentVariablesEnvironments", () => { expect(result.hasStaging).toBe(true); }); - postgresTest("returns hasStaging false when no staging environment exists", async ({ prisma }) => { - const { user, organization, project } = await createTestOrgProjectWithMember(prisma); + postgresTest( + "returns hasStaging false when no staging environment exists", + async ({ prisma }) => { + const { user, organization, project } = await createTestOrgProjectWithMember(prisma); - await createRuntimeEnvironment(prisma, { - projectId: project.id, - organizationId: organization.id, - type: "PRODUCTION", - }); + await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PRODUCTION", + }); - const result = await loadEnvironmentVariablesEnvironments(prisma, { - userId: user.id, - projectId: project.id, - }); + const result = await loadEnvironmentVariablesEnvironments(prisma, { + userId: user.id, + projectId: project.id, + }); - expect(result.hasStaging).toBe(false); - }); + expect(result.hasStaging).toBe(false); + } + ); }); diff --git a/apps/webapp/test/environmentVariablesRepository.test.ts b/apps/webapp/test/environmentVariablesRepository.test.ts index ead865eb5d..4025d8fd2e 100644 --- a/apps/webapp/test/environmentVariablesRepository.test.ts +++ b/apps/webapp/test/environmentVariablesRepository.test.ts @@ -11,9 +11,7 @@ vi.mock("~/db.server", () => ({ fnOrOptions?: ((tx: unknown) => Promise) | unknown ) => { const fn = - typeof nameOrFn === "string" - ? (fnOrOptions as (tx: unknown) => Promise) - : nameOrFn; + typeof nameOrFn === "string" ? (fnOrOptions as (tx: unknown) => Promise) : nameOrFn; return prismaClient.$transaction(fn); }, @@ -58,46 +56,49 @@ describe("EnvironmentVariablesRepository.getVariableValuesForKeys", () => { expect(result.has(`${environment.id}:DOES_NOT_EXIST`)).toBe(false); }); - postgresTest("returns requested values with correct map keys and decrypted values", async ({ prisma }) => { - const { user, organization, project } = await createTestOrgProjectWithMember(prisma); - const environment = await createRuntimeEnvironment(prisma, { - projectId: project.id, - organizationId: organization.id, - type: "PRODUCTION", - }); - - const repository = new EnvironmentVariablesRepository(prisma, prisma); - - await createEnvironmentVariable(repository, project.id, { - environmentId: environment.id, - key: "VAR_A", - value: "value-a", - userId: user.id, - }); - await createEnvironmentVariable(repository, project.id, { - environmentId: environment.id, - key: "VAR_B", - value: "value-b", - userId: user.id, - }); - await createEnvironmentVariable(repository, project.id, { - environmentId: environment.id, - key: "VAR_C", - value: "value-c", - userId: user.id, - }); - - const result = await repository.getVariableValuesForKeys(project.id, [ - { environmentId: environment.id, key: "VAR_A" }, - { environmentId: environment.id, key: "VAR_C" }, - ]); - - expect(result).toBeInstanceOf(Map); - expect(result.size).toBe(2); - expect(result.get(`${environment.id}:VAR_A`)).toBe("value-a"); - expect(result.get(`${environment.id}:VAR_C`)).toBe("value-c"); - expect(result.has(`${environment.id}:VAR_B`)).toBe(false); - }); + postgresTest( + "returns requested values with correct map keys and decrypted values", + async ({ prisma }) => { + const { user, organization, project } = await createTestOrgProjectWithMember(prisma); + const environment = await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PRODUCTION", + }); + + const repository = new EnvironmentVariablesRepository(prisma, prisma); + + await createEnvironmentVariable(repository, project.id, { + environmentId: environment.id, + key: "VAR_A", + value: "value-a", + userId: user.id, + }); + await createEnvironmentVariable(repository, project.id, { + environmentId: environment.id, + key: "VAR_B", + value: "value-b", + userId: user.id, + }); + await createEnvironmentVariable(repository, project.id, { + environmentId: environment.id, + key: "VAR_C", + value: "value-c", + userId: user.id, + }); + + const result = await repository.getVariableValuesForKeys(project.id, [ + { environmentId: environment.id, key: "VAR_A" }, + { environmentId: environment.id, key: "VAR_C" }, + ]); + + expect(result).toBeInstanceOf(Map); + expect(result.size).toBe(2); + expect(result.get(`${environment.id}:VAR_A`)).toBe("value-a"); + expect(result.get(`${environment.id}:VAR_C`)).toBe("value-c"); + expect(result.has(`${environment.id}:VAR_B`)).toBe(false); + } + ); postgresTest("deduplicates duplicate environmentId and key requests", async ({ prisma }) => { const { user, organization, project } = await createTestOrgProjectWithMember(prisma); @@ -117,7 +118,11 @@ describe("EnvironmentVariablesRepository.getVariableValuesForKeys", () => { }); const request = { environmentId: environment.id, key: "DEDUP_KEY" }; - const result = await repository.getVariableValuesForKeys(project.id, [request, request, request]); + const result = await repository.getVariableValuesForKeys(project.id, [ + request, + request, + request, + ]); expect(result.size).toBe(1); expect(result.get(`${environment.id}:DEDUP_KEY`)).toBe("dedup-value"); diff --git a/apps/webapp/test/errorFingerprinting.test.ts b/apps/webapp/test/errorFingerprinting.test.ts index 4d68fd87e8..b478825293 100644 --- a/apps/webapp/test/errorFingerprinting.test.ts +++ b/apps/webapp/test/errorFingerprinting.test.ts @@ -156,18 +156,13 @@ describe("normalizeErrorMessage", () => { }); it("message with both a URL and a timestamp", () => { - const message = - "Request to https://api.example.com/data failed at 2025-06-15T10:30:00Z"; - expect(normalizeErrorMessage(message)).toBe( - "Request to failed at " - ); + const message = "Request to https://api.example.com/data failed at 2025-06-15T10:30:00Z"; + expect(normalizeErrorMessage(message)).toBe("Request to failed at "); }); it("message with a URL and a unix timestamp", () => { const message = "Callback to https://example.com/hook timed out after 1700000000"; - expect(normalizeErrorMessage(message)).toBe( - "Callback to timed out after " - ); + expect(normalizeErrorMessage(message)).toBe("Callback to timed out after "); }); it("path-like string that is NOT a URL should still become ", () => { diff --git a/apps/webapp/test/fairDequeuingStrategy.test.ts b/apps/webapp/test/fairDequeuingStrategy.test.ts index 0d8b708161..f5b96cf5e8 100644 --- a/apps/webapp/test/fairDequeuingStrategy.test.ts +++ b/apps/webapp/test/fairDequeuingStrategy.test.ts @@ -527,14 +527,17 @@ describe("FairDequeuingStrategy", () => { const result = flattenResults(envResult); // Group queues by environment - const queuesByEnv = result.reduce((acc, queueId) => { - const envId = keyProducer.envIdFromQueue(queueId); - if (!acc[envId]) { - acc[envId] = []; - } - acc[envId].push(queueId); - return acc; - }, {} as Record); + const queuesByEnv = result.reduce( + (acc, queueId) => { + const envId = keyProducer.envIdFromQueue(queueId); + if (!acc[envId]) { + acc[envId] = []; + } + acc[envId].push(queueId); + return acc; + }, + {} as Record + ); // Verify that: // 1. We got all queues diff --git a/apps/webapp/test/findEnvironmentByApiKey.test.ts b/apps/webapp/test/findEnvironmentByApiKey.test.ts index 22477207b5..5bbc1e9783 100644 --- a/apps/webapp/test/findEnvironmentByApiKey.test.ts +++ b/apps/webapp/test/findEnvironmentByApiKey.test.ts @@ -40,54 +40,57 @@ async function createEnv( } describe("findEnvironmentByApiKey β€” DEVELOPMENT branch resolution", () => { - postgresTest("resolves the full dev auth matrix from the parent's api key", async ({ prisma }) => { - const { organization, project, orgMember } = await createTestOrgProjectWithMember(prisma); - - // The existing per-member dev env IS the default branch (no branchName). - const devRoot = await createEnv(prisma, project.id, organization.id, { - type: "DEVELOPMENT", - orgMemberId: orgMember.id, - }); - - const namedBranch = await createEnv(prisma, project.id, organization.id, { - type: "DEVELOPMENT", - orgMemberId: orgMember.id, - parentEnvironmentId: devRoot.id, - branchName: "my-feature", - }); - - await createEnv(prisma, project.id, organization.id, { - type: "DEVELOPMENT", - orgMemberId: orgMember.id, - parentEnvironmentId: devRoot.id, - branchName: "archived-feature", - archivedAt: new Date(), - }); - - // No header β†’ the root dev env (unchanged, day-one behaviour). - const noHeader = await findEnvironmentByApiKey(devRoot.apiKey, undefined, prisma); - expect(noHeader?.id).toBe(devRoot.id); - - // "default" sentinel β†’ also the root dev env. - const defaultHeader = await findEnvironmentByApiKey(devRoot.apiKey, "default", prisma); - expect(defaultHeader?.id).toBe(devRoot.id); - - // A named branch that exists β†’ the child env... - const child = await findEnvironmentByApiKey(devRoot.apiKey, "my-feature", prisma); - expect(child?.id).toBe(namedBranch.id); - expect(child?.branchName).toBe("my-feature"); - // ...but carrying the PARENT's api key and ownership, not the child's own key. - expect(child?.apiKey).toBe(devRoot.apiKey); - expect(child?.orgMemberId).toBe(orgMember.id); - - // A named branch that doesn't exist β†’ null (not a silent fall-through to root). - const missing = await findEnvironmentByApiKey(devRoot.apiKey, "does-not-exist", prisma); - expect(missing).toBeNull(); - - // An archived branch β†’ null (archivedAt filter on the child include). - const archived = await findEnvironmentByApiKey(devRoot.apiKey, "archived-feature", prisma); - expect(archived).toBeNull(); - }); + postgresTest( + "resolves the full dev auth matrix from the parent's api key", + async ({ prisma }) => { + const { organization, project, orgMember } = await createTestOrgProjectWithMember(prisma); + + // The existing per-member dev env IS the default branch (no branchName). + const devRoot = await createEnv(prisma, project.id, organization.id, { + type: "DEVELOPMENT", + orgMemberId: orgMember.id, + }); + + const namedBranch = await createEnv(prisma, project.id, organization.id, { + type: "DEVELOPMENT", + orgMemberId: orgMember.id, + parentEnvironmentId: devRoot.id, + branchName: "my-feature", + }); + + await createEnv(prisma, project.id, organization.id, { + type: "DEVELOPMENT", + orgMemberId: orgMember.id, + parentEnvironmentId: devRoot.id, + branchName: "archived-feature", + archivedAt: new Date(), + }); + + // No header β†’ the root dev env (unchanged, day-one behaviour). + const noHeader = await findEnvironmentByApiKey(devRoot.apiKey, undefined, prisma); + expect(noHeader?.id).toBe(devRoot.id); + + // "default" sentinel β†’ also the root dev env. + const defaultHeader = await findEnvironmentByApiKey(devRoot.apiKey, "default", prisma); + expect(defaultHeader?.id).toBe(devRoot.id); + + // A named branch that exists β†’ the child env... + const child = await findEnvironmentByApiKey(devRoot.apiKey, "my-feature", prisma); + expect(child?.id).toBe(namedBranch.id); + expect(child?.branchName).toBe("my-feature"); + // ...but carrying the PARENT's api key and ownership, not the child's own key. + expect(child?.apiKey).toBe(devRoot.apiKey); + expect(child?.orgMemberId).toBe(orgMember.id); + + // A named branch that doesn't exist β†’ null (not a silent fall-through to root). + const missing = await findEnvironmentByApiKey(devRoot.apiKey, "does-not-exist", prisma); + expect(missing).toBeNull(); + + // An archived branch β†’ null (archivedAt filter on the child include). + const archived = await findEnvironmentByApiKey(devRoot.apiKey, "archived-feature", prisma); + expect(archived).toBeNull(); + } + ); postgresTest("a branch name is sanitized before lookup", async ({ prisma }) => { const { organization, project, orgMember } = await createTestOrgProjectWithMember(prisma); @@ -114,39 +117,45 @@ describe("findEnvironmentByApiKey β€” DEVELOPMENT branch resolution", () => { }); describe("findEnvironmentByApiKey β€” PREVIEW (regression guard)", () => { - postgresTest("preview still requires a branch and never resolves the parent", async ({ prisma }) => { - const { organization, project } = await createTestOrgProjectWithMember(prisma); - - const previewParent = await createEnv(prisma, project.id, organization.id, { - type: "PREVIEW", - isBranchableEnvironment: true, - }); - const previewBranch = await createEnv(prisma, project.id, organization.id, { - type: "PREVIEW", - parentEnvironmentId: previewParent.id, - branchName: "pr-123", - }); - - // No header on a preview key β†’ null (preview has no default). - const noHeader = await findEnvironmentByApiKey(previewParent.apiKey, undefined, prisma); - expect(noHeader).toBeNull(); - - // With a branch β†’ the child, carrying the parent's api key. - const resolved = await findEnvironmentByApiKey(previewParent.apiKey, "pr-123", prisma); - expect(resolved?.id).toBe(previewBranch.id); - expect(resolved?.apiKey).toBe(previewParent.apiKey); - }); + postgresTest( + "preview still requires a branch and never resolves the parent", + async ({ prisma }) => { + const { organization, project } = await createTestOrgProjectWithMember(prisma); + + const previewParent = await createEnv(prisma, project.id, organization.id, { + type: "PREVIEW", + isBranchableEnvironment: true, + }); + const previewBranch = await createEnv(prisma, project.id, organization.id, { + type: "PREVIEW", + parentEnvironmentId: previewParent.id, + branchName: "pr-123", + }); + + // No header on a preview key β†’ null (preview has no default). + const noHeader = await findEnvironmentByApiKey(previewParent.apiKey, undefined, prisma); + expect(noHeader).toBeNull(); + + // With a branch β†’ the child, carrying the parent's api key. + const resolved = await findEnvironmentByApiKey(previewParent.apiKey, "pr-123", prisma); + expect(resolved?.id).toBe(previewBranch.id); + expect(resolved?.apiKey).toBe(previewParent.apiKey); + } + ); }); describe("findEnvironmentByApiKey β€” non-branchable", () => { - postgresTest("a production key ignores the branch header and returns itself", async ({ prisma }) => { - const { organization, project } = await createTestOrgProjectWithMember(prisma); + postgresTest( + "a production key ignores the branch header and returns itself", + async ({ prisma }) => { + const { organization, project } = await createTestOrgProjectWithMember(prisma); - const prod = await createEnv(prisma, project.id, organization.id, { type: "PRODUCTION" }); + const prod = await createEnv(prisma, project.id, organization.id, { type: "PRODUCTION" }); - const resolved = await findEnvironmentByApiKey(prod.apiKey, "some-branch", prisma); - expect(resolved?.id).toBe(prod.id); - }); + const resolved = await findEnvironmentByApiKey(prod.apiKey, "some-branch", prisma); + expect(resolved?.id).toBe(prod.id); + } + ); postgresTest("an unknown api key returns null", async ({ prisma }) => { const resolved = await findEnvironmentByApiKey("tr_dev_nonexistent", undefined, prisma); diff --git a/apps/webapp/test/healthcheck-require-plugins.e2e.test.ts b/apps/webapp/test/healthcheck-require-plugins.e2e.test.ts index 17e02ce8d9..e16801babb 100644 --- a/apps/webapp/test/healthcheck-require-plugins.e2e.test.ts +++ b/apps/webapp/test/healthcheck-require-plugins.e2e.test.ts @@ -23,13 +23,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import type { TestServer } from "@internal/testcontainers/webapp"; import { startTestServer } from "@internal/testcontainers/webapp"; -const LINKED_PLUGIN_PATH = resolve( - __dirname, - "..", - "node_modules", - "@triggerdotdev", - "plugins" -); +const LINKED_PLUGIN_PATH = resolve(__dirname, "..", "node_modules", "@triggerdotdev", "plugins"); const pluginLocallyLinked = existsSync(LINKED_PLUGIN_PATH); vi.setConfig({ testTimeout: 180_000 }); @@ -63,10 +57,7 @@ describe("/healthcheck with REQUIRE_PLUGINS", () => { describe.runIf(pluginLocallyLinked)( "REQUIRE_PLUGINS=1 + plugin LOCALLY LINKED (cross-repo dev setup)", () => { - it.skip( - `skipped because ${LINKED_PLUGIN_PATH} exists β€” plugin would load successfully. Run \`pnpm dev:unlink-webapp\` to exercise this case locally; CI runs it without the link.`, - () => {} - ); + it.skip(`skipped because ${LINKED_PLUGIN_PATH} exists β€” plugin would load successfully. Run \`pnpm dev:unlink-webapp\` to exercise this case locally; CI runs it without the link.`, () => {}); } ); diff --git a/apps/webapp/test/helpers/seedTestApiSession.ts b/apps/webapp/test/helpers/seedTestApiSession.ts index cb98c1798c..b20c21b9df 100644 --- a/apps/webapp/test/helpers/seedTestApiSession.ts +++ b/apps/webapp/test/helpers/seedTestApiSession.ts @@ -10,7 +10,9 @@ import type { PrismaClient, Session } from "@trigger.dev/database"; import { randomBytes } from "node:crypto"; function randomHex(len = 12): string { - return randomBytes(Math.ceil(len / 2)).toString("hex").slice(0, len); + return randomBytes(Math.ceil(len / 2)) + .toString("hex") + .slice(0, len); } export async function seedTestApiSession( @@ -32,9 +34,7 @@ export async function seedTestApiSession( // (single-id auth resource); omit the override to get a unique // externalId for the multi-key path. externalId: - overrides?.externalId === null - ? null - : overrides?.externalId ?? `ext_${suffix}`, + overrides?.externalId === null ? null : (overrides?.externalId ?? `ext_${suffix}`), type: "chat.agent", projectId: env.projectId, runtimeEnvironmentId: env.id, diff --git a/apps/webapp/test/helpers/seedTestEnvironment.ts b/apps/webapp/test/helpers/seedTestEnvironment.ts index 670927cc62..beca226134 100644 --- a/apps/webapp/test/helpers/seedTestEnvironment.ts +++ b/apps/webapp/test/helpers/seedTestEnvironment.ts @@ -2,7 +2,9 @@ import type { PrismaClient } from "@trigger.dev/database"; import { randomBytes } from "crypto"; function randomHex(len = 12): string { - return randomBytes(Math.ceil(len / 2)).toString("hex").slice(0, len); + return randomBytes(Math.ceil(len / 2)) + .toString("hex") + .slice(0, len); } export async function seedTestEnvironment(prisma: PrismaClient) { diff --git a/apps/webapp/test/helpers/seedTestUserProject.ts b/apps/webapp/test/helpers/seedTestUserProject.ts index 3512054ec1..cf8ae86261 100644 --- a/apps/webapp/test/helpers/seedTestUserProject.ts +++ b/apps/webapp/test/helpers/seedTestUserProject.ts @@ -3,7 +3,9 @@ import { randomBytes } from "node:crypto"; import { seedTestPAT, seedTestUser } from "./seedTestPAT"; function randomHex(len = 12): string { - return randomBytes(Math.ceil(len / 2)).toString("hex").slice(0, len); + return randomBytes(Math.ceil(len / 2)) + .toString("hex") + .slice(0, len); } // Composite test fixture: a User, an Organization with that user as a diff --git a/apps/webapp/test/metadataRouteOperationsLogging.test.ts b/apps/webapp/test/metadataRouteOperationsLogging.test.ts index ab96c9b9b2..b7e5f86019 100644 --- a/apps/webapp/test/metadataRouteOperationsLogging.test.ts +++ b/apps/webapp/test/metadataRouteOperationsLogging.test.ts @@ -26,7 +26,10 @@ vi.mock("~/services/apiAuth.server", () => ({ })); vi.mock("~/v3/services/common.server", () => ({ ServiceValidationError: class extends Error { - constructor(public override message: string, public status?: number) { + constructor( + public override message: string, + public status?: number + ) { super(message); } }, @@ -77,7 +80,7 @@ describe("routeOperationsToRun β€” non-throw buffer outcome logging", () => { expect(warnSpy).toHaveBeenCalledWith( "metadata route: parent/root buffer op did not apply", - expect.objectContaining({ targetRunId: "run_buffered_1", kind }), + expect.objectContaining({ targetRunId: "run_buffered_1", kind }) ); }); } @@ -95,7 +98,7 @@ describe("routeOperationsToRun β€” non-throw buffer outcome logging", () => { expect(warnSpy).not.toHaveBeenCalledWith( "metadata route: parent/root buffer op did not apply", - expect.anything(), + expect.anything() ); }); @@ -110,11 +113,11 @@ describe("routeOperationsToRun β€” non-throw buffer outcome logging", () => { // early on bufferError). expect(warnSpy).toHaveBeenCalledWith( "metadata route: buffer fallback for parent/root op failed", - expect.objectContaining({ targetRunId: "run_buffered_1" }), + expect.objectContaining({ targetRunId: "run_buffered_1" }) ); expect(warnSpy).not.toHaveBeenCalledWith( "metadata route: parent/root buffer op did not apply", - expect.anything(), + expect.anything() ); }); diff --git a/apps/webapp/test/mollifierApplyMetadataMutation.test.ts b/apps/webapp/test/mollifierApplyMetadataMutation.test.ts index 5995f6969f..474c9864cd 100644 --- a/apps/webapp/test/mollifierApplyMetadataMutation.test.ts +++ b/apps/webapp/test/mollifierApplyMetadataMutation.test.ts @@ -50,11 +50,13 @@ function makeBufferStub(initialPayload: Record = {}): BufferStu }; const buffer: MollifierBuffer = { - getEntry: vi.fn(async (): Promise => ({ - ...entryTemplate, - metadataVersion: state.version, - payload: JSON.stringify({ ...initialPayload, metadata: JSON.stringify(state.metadata) }), - })), + getEntry: vi.fn( + async (): Promise => ({ + ...entryTemplate, + metadataVersion: state.version, + payload: JSON.stringify({ ...initialPayload, metadata: JSON.stringify(state.metadata) }), + }) + ), casSetMetadata: vi.fn( async (input: { runId: string; @@ -75,7 +77,7 @@ function makeBufferStub(initialPayload: Record = {}): BufferStu state.metadata = JSON.parse(input.newMetadata) as Record; state.version += 1; return { kind: "applied", newVersion: state.version }; - }, + } ), } as unknown as MollifierBuffer; @@ -119,7 +121,10 @@ describe("applyMetadataMutationToBufferedRun β€” retry behaviour", () => { const { buffer } = makeBufferStub(); const setStateConflicts = (n: number) => { // Re-read state from the closure - const state = (buffer as unknown as { __state__?: never; getEntry: () => Promise }); + const state = buffer as unknown as { + __state__?: never; + getEntry: () => Promise; + }; void state; }; void setStateConflicts; @@ -342,7 +347,7 @@ describe("applyMetadataMutationToBufferedRun β€” retry behaviour", () => { maximumSize: 1024 * 1024, body: { operations: [{ type: "increment", key: "counter", value: 1 }] }, buffer: sharedStub.buffer, - }), + }) ); const results = await Promise.all(calls); const applied = results.filter((r) => r.kind === "applied").length; diff --git a/apps/webapp/test/mollifierClaimResolution.test.ts b/apps/webapp/test/mollifierClaimResolution.test.ts index f61cda0d04..7a2a0c1e54 100644 --- a/apps/webapp/test/mollifierClaimResolution.test.ts +++ b/apps/webapp/test/mollifierClaimResolution.test.ts @@ -31,7 +31,7 @@ function makeConcern(prisma: { findFirst: () => Promise }) { return new IdempotencyKeyConcern( { taskRun: { findFirst: prisma.findFirst } } as never, {} as never, // engine β€” unused on this path - {} as never, // traceEventConcern β€” unused on this path + {} as never // traceEventConcern β€” unused on this path ); } diff --git a/apps/webapp/test/mollifierDecisionLabels.test.ts b/apps/webapp/test/mollifierDecisionLabels.test.ts index 8206edabb0..69c67c5af7 100644 --- a/apps/webapp/test/mollifierDecisionLabels.test.ts +++ b/apps/webapp/test/mollifierDecisionLabels.test.ts @@ -27,7 +27,7 @@ describe("decisionLabels", () => { // Enrolled: org label present. expect( - decisionLabels("mollify", { enrolled: true, orgId: "org_1", reason: "per_env_rate" }), + decisionLabels("mollify", { enrolled: true, orgId: "org_1", reason: "per_env_rate" }) ).toEqual({ outcome: "mollify", enrolled: "true", @@ -45,10 +45,10 @@ describe("decisionLabels", () => { it("includes `reason` only when supplied", () => { expect(decisionLabels("pass_through", { enrolled: true, orgId: "org_1" })).not.toHaveProperty( - "reason", + "reason" + ); + expect(decisionLabels("shadow_log", { enrolled: false, reason: "per_env_rate" })).toMatchObject( + { reason: "per_env_rate" } ); - expect( - decisionLabels("shadow_log", { enrolled: false, reason: "per_env_rate" }), - ).toMatchObject({ reason: "per_env_rate" }); }); }); diff --git a/apps/webapp/test/mollifierDrainerHandler.test.ts b/apps/webapp/test/mollifierDrainerHandler.test.ts index 085fab6418..c4897f6165 100644 --- a/apps/webapp/test/mollifierDrainerHandler.test.ts +++ b/apps/webapp/test/mollifierDrainerHandler.test.ts @@ -138,7 +138,7 @@ describe("createDrainerHandler", () => { payload: { taskIdentifier: "t" }, attempts: 0, createdAt: new Date(), - } as any), + } as any) ).rejects.toThrow("Can't reach database server"); // Retryable: we do NOT write a SYSTEM_FAILURE row, the entry should // be requeued for another shot. @@ -173,7 +173,7 @@ describe("createDrainerHandler", () => { payload: { taskIdentifier: "t", environment: envFixture }, attempts: 0, createdAt: new Date(), - } as any), + } as any) ).resolves.toBeUndefined(); expect(trigger).toHaveBeenCalledOnce(); @@ -213,7 +213,7 @@ describe("createDrainerHandler", () => { }, attempts: 0, createdAt: new Date(), - } as any), + } as any) ).resolves.toBeUndefined(); expect(createFailedTaskRun).toHaveBeenCalledOnce(); @@ -244,7 +244,7 @@ describe("createDrainerHandler", () => { payload: { taskIdentifier: "t", environment: envFixture }, attempts: 0, createdAt: new Date(), - } as any), + } as any) ).rejects.toThrow("engine rejected the snapshot"); // Drainer's outer drainOne loop now decides retry vs buffer.fail. expect(createFailedTaskRun).toHaveBeenCalledOnce(); @@ -301,9 +301,7 @@ describe("createDrainerHandler", () => { // while the run keeps executing. const friendlyId = RunId.generate().friendlyId; const createCancelledRun = vi.fn(async () => { - throw new Error( - `createCancelledRun conflict: existing run ${friendlyId} has status PENDING` - ); + throw new Error(`createCancelledRun conflict: existing run ${friendlyId} has status PENDING`); }); const cancelRun = vi.fn(async () => ({ alreadyFinished: false })); const handler = createDrainerHandler({ @@ -462,7 +460,7 @@ describe("createDrainerHandler", () => { payload: { taskIdentifier: "t" /* no environment */ }, attempts: 0, createdAt: new Date(), - } as any), + } as any) ).rejects.toThrow("engine rejected the snapshot"); expect(createFailedTaskRun).not.toHaveBeenCalled(); }); diff --git a/apps/webapp/test/mollifierDrainerWorker.test.ts b/apps/webapp/test/mollifierDrainerWorker.test.ts index 0d4e931fd8..bf17b0e992 100644 --- a/apps/webapp/test/mollifierDrainerWorker.test.ts +++ b/apps/webapp/test/mollifierDrainerWorker.test.ts @@ -28,7 +28,7 @@ import { initMollifierDrainerWorker } from "~/v3/mollifierDrainerWorker.server"; describe("initMollifierDrainerWorker error classification", () => { it("rethrows MollifierConfigurationError so the process can crash on misconfig", () => { const misconfig = new MollifierConfigurationError( - "TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS must be at least 1000ms below GRACEFUL_SHUTDOWN_TIMEOUT", + "TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS must be at least 1000ms below GRACEFUL_SHUTDOWN_TIMEOUT" ); expect(() => @@ -37,7 +37,7 @@ describe("initMollifierDrainerWorker error classification", () => { getDrainer: () => { throw misconfig; }, - }), + }) ).toThrow(MollifierConfigurationError); }); @@ -56,7 +56,7 @@ describe("initMollifierDrainerWorker error classification", () => { getDrainer: () => { throw cousin; }, - }), + }) ).toThrow(cousin); }); @@ -67,7 +67,7 @@ describe("initMollifierDrainerWorker error classification", () => { getDrainer: () => { throw new Error("transient redis blip during buffer init"); }, - }), + }) ).not.toThrow(); }); diff --git a/apps/webapp/test/mollifierGate.test.ts b/apps/webapp/test/mollifierGate.test.ts index 5f3f28e668..1cc4626177 100644 --- a/apps/webapp/test/mollifierGate.test.ts +++ b/apps/webapp/test/mollifierGate.test.ts @@ -26,7 +26,10 @@ import type { DecisionOutcome, DecisionReason } from "~/v3/mollifier/mollifierTe type Spies = { evaluatorCalls: number; logShadowCalls: Array<{ inputs: GateInputs; decision: Extract }>; - logMollifiedCalls: Array<{ inputs: GateInputs; decision: Extract }>; + logMollifiedCalls: Array<{ + inputs: GateInputs; + decision: Extract; + }>; recordDecisionCalls: Array<{ outcome: DecisionOutcome; reason?: DecisionReason; @@ -118,26 +121,250 @@ type Row = { // each row exercises so reviewers can map row β†’ code at a glance. const cascade: Row[] = [ // enabled=F β†’ kill-switch wins; evaluator+flag never consulted (rows 1-8) - { id: 1, enabled: false, shadow: false, flag: false, divert: false, expected: { action: "pass_through", evaluatorCalls: 0, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, - { id: 2, enabled: false, shadow: false, flag: false, divert: true, expected: { action: "pass_through", evaluatorCalls: 0, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, - { id: 3, enabled: false, shadow: false, flag: true, divert: false, expected: { action: "pass_through", evaluatorCalls: 0, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, - { id: 4, enabled: false, shadow: false, flag: true, divert: true, expected: { action: "pass_through", evaluatorCalls: 0, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, - { id: 5, enabled: false, shadow: true, flag: false, divert: false, expected: { action: "pass_through", evaluatorCalls: 0, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, - { id: 6, enabled: false, shadow: true, flag: false, divert: true, expected: { action: "pass_through", evaluatorCalls: 0, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, - { id: 7, enabled: false, shadow: true, flag: true, divert: false, expected: { action: "pass_through", evaluatorCalls: 0, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, - { id: 8, enabled: false, shadow: true, flag: true, divert: true, expected: { action: "pass_through", evaluatorCalls: 0, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, + { + id: 1, + enabled: false, + shadow: false, + flag: false, + divert: false, + expected: { + action: "pass_through", + evaluatorCalls: 0, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, + { + id: 2, + enabled: false, + shadow: false, + flag: false, + divert: true, + expected: { + action: "pass_through", + evaluatorCalls: 0, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, + { + id: 3, + enabled: false, + shadow: false, + flag: true, + divert: false, + expected: { + action: "pass_through", + evaluatorCalls: 0, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, + { + id: 4, + enabled: false, + shadow: false, + flag: true, + divert: true, + expected: { + action: "pass_through", + evaluatorCalls: 0, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, + { + id: 5, + enabled: false, + shadow: true, + flag: false, + divert: false, + expected: { + action: "pass_through", + evaluatorCalls: 0, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, + { + id: 6, + enabled: false, + shadow: true, + flag: false, + divert: true, + expected: { + action: "pass_through", + evaluatorCalls: 0, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, + { + id: 7, + enabled: false, + shadow: true, + flag: true, + divert: false, + expected: { + action: "pass_through", + evaluatorCalls: 0, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, + { + id: 8, + enabled: false, + shadow: true, + flag: true, + divert: true, + expected: { + action: "pass_through", + evaluatorCalls: 0, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, // enabled=T, flag=F, shadow=F β†’ both opt-ins off; evaluator never called (rows 9-10) - { id: 9, enabled: true, shadow: false, flag: false, divert: false, expected: { action: "pass_through", evaluatorCalls: 0, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, - { id: 10, enabled: true, shadow: false, flag: false, divert: true, expected: { action: "pass_through", evaluatorCalls: 0, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, + { + id: 9, + enabled: true, + shadow: false, + flag: false, + divert: false, + expected: { + action: "pass_through", + evaluatorCalls: 0, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, + { + id: 10, + enabled: true, + shadow: false, + flag: false, + divert: true, + expected: { + action: "pass_through", + evaluatorCalls: 0, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, // enabled=T, flag=F, shadow=T β†’ shadow path; divert routes outcome (rows 11-12) - { id: 11, enabled: true, shadow: true, flag: false, divert: false, expected: { action: "pass_through", evaluatorCalls: 1, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, - { id: 12, enabled: true, shadow: true, flag: false, divert: true, expected: { action: "shadow_log", evaluatorCalls: 1, logShadowCalls: 1, logMollifiedCalls: 0, recordedOutcome: "shadow_log", expectedReason: "per_env_rate" } }, + { + id: 11, + enabled: true, + shadow: true, + flag: false, + divert: false, + expected: { + action: "pass_through", + evaluatorCalls: 1, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, + { + id: 12, + enabled: true, + shadow: true, + flag: false, + divert: true, + expected: { + action: "shadow_log", + evaluatorCalls: 1, + logShadowCalls: 1, + logMollifiedCalls: 0, + recordedOutcome: "shadow_log", + expectedReason: "per_env_rate", + }, + }, // enabled=T, flag=T, shadow=F β†’ mollify path (rows 13-14) - { id: 13, enabled: true, shadow: false, flag: true, divert: false, expected: { action: "pass_through", evaluatorCalls: 1, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, - { id: 14, enabled: true, shadow: false, flag: true, divert: true, expected: { action: "mollify", evaluatorCalls: 1, logShadowCalls: 0, logMollifiedCalls: 1, recordedOutcome: "mollify", expectedReason: "per_env_rate" } }, + { + id: 13, + enabled: true, + shadow: false, + flag: true, + divert: false, + expected: { + action: "pass_through", + evaluatorCalls: 1, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, + { + id: 14, + enabled: true, + shadow: false, + flag: true, + divert: true, + expected: { + action: "mollify", + evaluatorCalls: 1, + logShadowCalls: 0, + logMollifiedCalls: 1, + recordedOutcome: "mollify", + expectedReason: "per_env_rate", + }, + }, // enabled=T, flag=T, shadow=T β†’ flag wins over shadow (rows 15-16) - { id: 15, enabled: true, shadow: true, flag: true, divert: false, expected: { action: "pass_through", evaluatorCalls: 1, logShadowCalls: 0, logMollifiedCalls: 0, recordedOutcome: "pass_through", expectedReason: undefined } }, - { id: 16, enabled: true, shadow: true, flag: true, divert: true, expected: { action: "mollify", evaluatorCalls: 1, logShadowCalls: 0, logMollifiedCalls: 1, recordedOutcome: "mollify", expectedReason: "per_env_rate" } }, + { + id: 15, + enabled: true, + shadow: true, + flag: true, + divert: false, + expected: { + action: "pass_through", + evaluatorCalls: 1, + logShadowCalls: 0, + logMollifiedCalls: 0, + recordedOutcome: "pass_through", + expectedReason: undefined, + }, + }, + { + id: 16, + enabled: true, + shadow: true, + flag: true, + divert: true, + expected: { + action: "mollify", + evaluatorCalls: 1, + logShadowCalls: 0, + logMollifiedCalls: 1, + recordedOutcome: "mollify", + expectedReason: "per_env_rate", + }, + }, ]; describe("evaluateGate cascade β€” exhaustive truth table", () => { @@ -168,7 +395,7 @@ describe("evaluateGate cascade β€” exhaustive truth table", () => { // mollifierDecisionLabels.test.ts). expect(spies.recordDecisionCalls[0].enrolled).toBe(row.flag); expect(spies.recordDecisionCalls[0].orgId).toBe(inputs.orgId); - }, + } ); it("divert log carries the full decision (envId, orgId, taskId, reason, count, threshold, windowMs, holdMs)", async () => { @@ -345,9 +572,10 @@ describe("evaluateGate β€” fail open on resolveOrgFlag error", () => { }); describe("evaluateGate β€” per-org isolation via Organization.featureFlags", () => { - function makeIsolationDeps( - resolveOrgFlag: GateDependencies["resolveOrgFlag"], - ): { deps: Partial; spies: Spies } { + function makeIsolationDeps(resolveOrgFlag: GateDependencies["resolveOrgFlag"]): { + deps: Partial; + spies: Spies; + } { const spies: Spies = { evaluatorCalls: 0, logShadowCalls: [], @@ -486,10 +714,7 @@ describe("evaluateGate β€” debounce / OTU / triggerAndWait bypasses", () => { flag: true, decision: trippedDecision, }); - const outcome = await evaluateGate( - { ...inputs, options: { debounce: { key: "k" } } }, - deps, - ); + const outcome = await evaluateGate({ ...inputs, options: { debounce: { key: "k" } } }, deps); expect(outcome).toEqual({ action: "pass_through" }); expect(spies.evaluatorCalls).toBe(0); }); @@ -503,7 +728,7 @@ describe("evaluateGate β€” debounce / OTU / triggerAndWait bypasses", () => { }); const outcome = await evaluateGate( { ...inputs, options: { oneTimeUseToken: "jwt-otu" } }, - deps, + deps ); expect(outcome).toEqual({ action: "pass_through" }); expect(spies.evaluatorCalls).toBe(0); @@ -521,7 +746,7 @@ describe("evaluateGate β€” debounce / OTU / triggerAndWait bypasses", () => { ...inputs, options: { parentTaskRunId: "run_parent", resumeParentOnCompletion: true }, }, - deps, + deps ); expect(outcome).toEqual({ action: "pass_through" }); expect(spies.evaluatorCalls).toBe(0); @@ -536,7 +761,7 @@ describe("evaluateGate β€” debounce / OTU / triggerAndWait bypasses", () => { }); const outcome = await evaluateGate( { ...inputs, options: { parentTaskRunId: "run_parent" } }, - deps, + deps ); expect(outcome.action).toBe("mollify"); expect(spies.evaluatorCalls).toBe(1); diff --git a/apps/webapp/test/mollifierIdempotencyClaim.test.ts b/apps/webapp/test/mollifierIdempotencyClaim.test.ts index 87c009cb1f..1b093250f1 100644 --- a/apps/webapp/test/mollifierIdempotencyClaim.test.ts +++ b/apps/webapp/test/mollifierIdempotencyClaim.test.ts @@ -2,15 +2,8 @@ import { describe, expect, it, vi } from "vitest"; vi.mock("~/db.server", () => ({ prisma: {}, $replica: {} })); -import { - claimOrAwait, - publishClaim, - releaseClaim, -} from "~/v3/mollifier/idempotencyClaim.server"; -import type { - IdempotencyClaimResult, - MollifierBuffer, -} from "@trigger.dev/redis-worker"; +import { claimOrAwait, publishClaim, releaseClaim } from "~/v3/mollifier/idempotencyClaim.server"; +import type { IdempotencyClaimResult, MollifierBuffer } from "@trigger.dev/redis-worker"; type ClaimState = { value: string | null; @@ -190,7 +183,7 @@ describe("publishClaim", () => { it("no-op when buffer is null", async () => { await expect( - publishClaim({ ...baseInput, token: "owner-token", runId: "run_X", buffer: null }), + publishClaim({ ...baseInput, token: "owner-token", runId: "run_X", buffer: null }) ).resolves.toBeUndefined(); }); @@ -201,7 +194,7 @@ describe("publishClaim", () => { }), } as unknown as MollifierBuffer; await expect( - publishClaim({ ...baseInput, token: "owner-token", runId: "run_X", buffer }), + publishClaim({ ...baseInput, token: "owner-token", runId: "run_X", buffer }) ).resolves.toBeUndefined(); }); }); @@ -214,7 +207,9 @@ describe("releaseClaim", () => { }); it("no-op when buffer is null", async () => { - await expect(releaseClaim({ ...baseInput, token: "owner-token", buffer: null })).resolves.toBeUndefined(); + await expect( + releaseClaim({ ...baseInput, token: "owner-token", buffer: null }) + ).resolves.toBeUndefined(); }); }); @@ -250,7 +245,7 @@ describe("claim ownership token wiring", () => { expect.objectContaining({ token: "owner-token-xyz", runId: "run_X", - }), + }) ); }); @@ -262,7 +257,7 @@ describe("claim ownership token wiring", () => { buffer, }); expect(buffer.releaseClaim).toHaveBeenCalledWith( - expect.objectContaining({ token: "owner-token-xyz" }), + expect.objectContaining({ token: "owner-token-xyz" }) ); }); }); diff --git a/apps/webapp/test/mollifierMollify.test.ts b/apps/webapp/test/mollifierMollify.test.ts index ec7a30b49c..fce4b93f64 100644 --- a/apps/webapp/test/mollifierMollify.test.ts +++ b/apps/webapp/test/mollifierMollify.test.ts @@ -10,7 +10,7 @@ import { RunId } from "@trigger.dev/core/v3/isomorphic"; import type { MollifierBuffer } from "@trigger.dev/redis-worker"; function fakeBuffer( - acceptResult: Awaited> = { kind: "accepted" }, + acceptResult: Awaited> = { kind: "accepted" } ): { buffer: MollifierBuffer; accept: ReturnType } { const accept = vi.fn(async () => acceptResult); return { diff --git a/apps/webapp/test/mollifierMutateWithFallback.test.ts b/apps/webapp/test/mollifierMutateWithFallback.test.ts index 1102229f56..1414772a1a 100644 --- a/apps/webapp/test/mollifierMutateWithFallback.test.ts +++ b/apps/webapp/test/mollifierMutateWithFallback.test.ts @@ -6,11 +6,7 @@ vi.mock("~/db.server", () => ({ })); import { mutateWithFallback } from "~/v3/mollifier/mutateWithFallback.server"; -import type { - BufferEntry, - MollifierBuffer, - MutateSnapshotResult, -} from "@trigger.dev/redis-worker"; +import type { BufferEntry, MollifierBuffer, MutateSnapshotResult } from "@trigger.dev/redis-worker"; import type { TaskRun } from "@trigger.dev/database"; type FindFirst = ReturnType; diff --git a/apps/webapp/test/mollifierReadFallback.test.ts b/apps/webapp/test/mollifierReadFallback.test.ts index feef6a420a..2c439ea0bc 100644 --- a/apps/webapp/test/mollifierReadFallback.test.ts +++ b/apps/webapp/test/mollifierReadFallback.test.ts @@ -21,7 +21,7 @@ describe("findRunByIdWithMollifierFallback", () => { it("returns null when buffer is unavailable (mollifier disabled)", async () => { const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => null }, + { getBuffer: () => null } ); expect(result).toBeNull(); }); @@ -29,7 +29,7 @@ describe("findRunByIdWithMollifierFallback", () => { it("returns null when no buffer entry exists", async () => { const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(null) }, + { getBuffer: () => fakeBuffer(null) } ); expect(result).toBeNull(); }); @@ -46,7 +46,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result).toBeNull(); }); @@ -63,7 +63,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result).toBeNull(); }); @@ -80,7 +80,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result).not.toBeNull(); expect(result!.friendlyId).toBe("run_1"); @@ -101,7 +101,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.status).toBe("QUEUED"); }); @@ -119,7 +119,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.status).toBe("FAILED"); expect(result!.error).toEqual({ code: "VALIDATION", message: "task not found" }); @@ -154,7 +154,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result).not.toBeNull(); expect(result!.payloadType).toBe("application/json"); @@ -188,7 +188,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.traceId).toBe("trace_abc"); expect(result!.spanId).toBe("span_xyz"); @@ -222,7 +222,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result).not.toBeNull(); expect(result!.idempotencyKeyOptions).toEqual({ @@ -256,7 +256,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result).not.toBeNull(); expect(result!.idempotencyKeyOptions).toBeUndefined(); @@ -274,7 +274,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.payloadType).toBeUndefined(); expect(result!.metadata).toBeUndefined(); @@ -310,7 +310,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result).not.toBeNull(); expect(result!.id).toBeTypeOf("string"); @@ -344,7 +344,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.batchId).toBe("batch_internal_cuid"); }); @@ -361,7 +361,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.batchId).toBeUndefined(); }); @@ -383,7 +383,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result).not.toBeNull(); expect(result!.status).toBe("QUEUED"); @@ -410,7 +410,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.status).toBe("CANCELED"); expect(result!.cancelledAt).toEqual(new Date(cancelledAtIso)); @@ -430,7 +430,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.runtimeEnvironmentId).toBe("env_a"); expect(result!.workerQueue).toBeUndefined(); @@ -457,7 +457,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.batchId).toBe("batch_internal_xyz"); }); @@ -479,7 +479,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.batchId).toBeUndefined(); }); @@ -509,7 +509,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.parentTaskRunFriendlyId).toBe(parent.friendlyId); expect(result!.rootTaskRunFriendlyId).toBe(root.friendlyId); @@ -527,7 +527,7 @@ describe("findRunByIdWithMollifierFallback", () => { }; const result = await findRunByIdWithMollifierFallback( { runId: "run_1", environmentId: "env_a", organizationId: "org_1" }, - { getBuffer: () => fakeBuffer(entry) }, + { getBuffer: () => fakeBuffer(entry) } ); expect(result!.parentTaskRunFriendlyId).toBeUndefined(); expect(result!.rootTaskRunFriendlyId).toBeUndefined(); diff --git a/apps/webapp/test/mollifierReplayPayloadShape.test.ts b/apps/webapp/test/mollifierReplayPayloadShape.test.ts index d2f098d708..e95fabaaf5 100644 --- a/apps/webapp/test/mollifierReplayPayloadShape.test.ts +++ b/apps/webapp/test/mollifierReplayPayloadShape.test.ts @@ -51,9 +51,7 @@ describe("mollifier replay payload shape", () => { payloadType: "application/json", }; - const roundTripped = deserialiseMollifierSnapshot( - serialiseMollifierSnapshot(triggerInput), - ); + const roundTripped = deserialiseMollifierSnapshot(serialiseMollifierSnapshot(triggerInput)); // This is exactly the call the replay loader makes: // prettyPrintPacket(run.payload, run.payloadType) @@ -63,7 +61,7 @@ describe("mollifier replay payload shape", () => { // JSON. const pretty = await prettyPrintPacket( roundTripped.payload, - roundTripped.payloadType as string, + roundTripped.payloadType as string ); expect(pretty).toBe(JSON.stringify(original, null, 2)); diff --git a/apps/webapp/test/mollifierResetIdempotencyKey.test.ts b/apps/webapp/test/mollifierResetIdempotencyKey.test.ts index 4909087d70..82280444c6 100644 --- a/apps/webapp/test/mollifierResetIdempotencyKey.test.ts +++ b/apps/webapp/test/mollifierResetIdempotencyKey.test.ts @@ -90,7 +90,7 @@ describe("ResetIdempotencyKeyService β€” buffer-outage handling", () => { const error = await service.call("ikey", "task", env).then( () => null, - (err) => err, + (err) => err ); expect(error).toBeInstanceOf(ServiceValidationError); expect(error.status).toBe(503); diff --git a/apps/webapp/test/mollifierStaleSweep.test.ts b/apps/webapp/test/mollifierStaleSweep.test.ts index 9492861111..eee1da2501 100644 --- a/apps/webapp/test/mollifierStaleSweep.test.ts +++ b/apps/webapp/test/mollifierStaleSweep.test.ts @@ -101,7 +101,7 @@ describe("runStaleSweepOnce β€” unit", () => { const spies = spyDeps(); const result = await runStaleSweepOnce( { staleThresholdMs: 1000 }, - { ...spies.deps, getBuffer: () => null, state: makeFakeState() }, + { ...spies.deps, getBuffer: () => null, state: makeFakeState() } ); expect(result).toEqual({ orgsScanned: 0, @@ -157,8 +157,8 @@ describe("runStaleSweepOnce β€” unit", () => { state: failingState, getBuffer: () => buffer, now: () => Date.now(), - }, - ), + } + ) ).rejects.toThrow("Redis read failed"); expect(readAttempts).toBe(1); @@ -220,7 +220,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { getBuffer: () => buffer, state, now: () => futureNow, - }, + } ); expect(result.envsScanned).toBe(2); @@ -250,7 +250,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -278,7 +278,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { const spies = spyDeps(); await runStaleSweepOnce( { staleThresholdMs: 60 * 1000 }, - { ...spies.deps, getBuffer: () => buffer, state }, + { ...spies.deps, getBuffer: () => buffer, state } ); expect(spies.snapshots).toHaveLength(1); // env_a has entries but none stale β†’ not in the snapshot. @@ -287,7 +287,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { await state.close(); await buffer.close(); } - }, + } ); redisTest( @@ -310,7 +310,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { const spies = spyDeps(); const result = await runStaleSweepOnce( { staleThresholdMs: 60 * 1000 }, - { ...spies.deps, getBuffer: () => buffer, state }, + { ...spies.deps, getBuffer: () => buffer, state } ); expect(result.staleCount).toBe(0); expect(spies.staleEntryCount).toBe(0); @@ -319,7 +319,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { await state.close(); await buffer.close(); } - }, + } ); redisTest( @@ -376,7 +376,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { await state.close(); await buffer.close(); } - }, + } ); redisTest( @@ -430,7 +430,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { await state.close(); await buffer.close(); } - }, + } ); redisTest( @@ -501,48 +501,44 @@ describe("runStaleSweepOnce β€” testcontainers", () => { await state.close(); await buffer.close(); } - }, + } ); - redisTest( - "scans across multiple orgs", - { timeout: 20_000 }, - async ({ redisOptions }) => { - // The drainer pops with org-level fairness, so the sweep must - // walk every org/env to surface stale entries across all of them - // β€” not just stop at the first env it finds. If a future refactor - // collapsed listOrgs/listEnvsForOrg into a single env-flat list, - // this test catches a regression there. - const buffer = new MollifierBuffer({ redisOptions }); - const state = new MollifierStaleSweepState({ redisOptions }); - try { - await buffer.accept({ - runId: "run_x", - envId: "env_x", - orgId: "org_x", - payload: JSON.stringify(SNAPSHOT), - }); - await buffer.accept({ - runId: "run_y", - envId: "env_y", - orgId: "org_y", - payload: JSON.stringify(SNAPSHOT), - }); - const futureNow = Date.now() + 5 * 60 * 1000; - const spies = spyDeps(); - const result = await runStaleSweepOnce( - { staleThresholdMs: 60 * 1000 }, - { ...spies.deps, getBuffer: () => buffer, state, now: () => futureNow }, - ); - expect(result.orgsScanned).toBe(2); - expect(result.envsScanned).toBe(2); - expect(result.staleCount).toBe(2); - } finally { - await state.close(); - await buffer.close(); - } - }, - ); + redisTest("scans across multiple orgs", { timeout: 20_000 }, async ({ redisOptions }) => { + // The drainer pops with org-level fairness, so the sweep must + // walk every org/env to surface stale entries across all of them + // β€” not just stop at the first env it finds. If a future refactor + // collapsed listOrgs/listEnvsForOrg into a single env-flat list, + // this test catches a regression there. + const buffer = new MollifierBuffer({ redisOptions }); + const state = new MollifierStaleSweepState({ redisOptions }); + try { + await buffer.accept({ + runId: "run_x", + envId: "env_x", + orgId: "org_x", + payload: JSON.stringify(SNAPSHOT), + }); + await buffer.accept({ + runId: "run_y", + envId: "env_y", + orgId: "org_y", + payload: JSON.stringify(SNAPSHOT), + }); + const futureNow = Date.now() + 5 * 60 * 1000; + const spies = spyDeps(); + const result = await runStaleSweepOnce( + { staleThresholdMs: 60 * 1000 }, + { ...spies.deps, getBuffer: () => buffer, state, now: () => futureNow } + ); + expect(result.orgsScanned).toBe(2); + expect(result.envsScanned).toBe(2); + expect(result.staleCount).toBe(2); + } finally { + await state.close(); + await buffer.close(); + } + }); redisTest( "state survives process restart: a second state instance picks up the cursor and counts", @@ -611,7 +607,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { await state2.close(); await buffer.close(); } - }, + } ); redisTest( @@ -672,7 +668,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { await state.close(); await buffer.close(); } - }, + } ); redisTest( @@ -690,7 +686,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { const spies = spyDeps(); const result = await runStaleSweepOnce( { staleThresholdMs: 60 * 1000, maxOrgsPerPass: 10 }, - { ...spies.deps, getBuffer: () => buffer, state }, + { ...spies.deps, getBuffer: () => buffer, state } ); expect(result).toEqual({ orgsScanned: 0, @@ -706,7 +702,7 @@ describe("runStaleSweepOnce β€” testcontainers", () => { await state.close(); await buffer.close(); } - }, + } ); redisTest( @@ -754,19 +750,23 @@ describe("runStaleSweepOnce β€” testcontainers", () => { await state.close(); await buffer.close(); } - }, + } ); }); describe("MollifierStaleSweepState β€” direct unit tests", () => { - redisTest("readCursor returns 0 when the key is absent", { timeout: 20_000 }, async ({ redisOptions }) => { - const state = new MollifierStaleSweepState({ redisOptions }); - try { - expect(await state.readCursor()).toBe(0); - } finally { - await state.close(); + redisTest( + "readCursor returns 0 when the key is absent", + { timeout: 20_000 }, + async ({ redisOptions }) => { + const state = new MollifierStaleSweepState({ redisOptions }); + try { + expect(await state.readCursor()).toBe(0); + } finally { + await state.close(); + } } - }); + ); redisTest( "writeCursor + readCursor round-trip; readCursor parses a non-numeric value as 0", @@ -784,7 +784,7 @@ describe("MollifierStaleSweepState β€” direct unit tests", () => { } finally { await state.close(); } - }, + } ); redisTest( @@ -812,7 +812,7 @@ describe("MollifierStaleSweepState β€” direct unit tests", () => { } finally { await state.close(); } - }, + } ); redisTest( @@ -834,7 +834,7 @@ describe("MollifierStaleSweepState β€” direct unit tests", () => { } finally { await state.close(); } - }, + } ); redisTest( @@ -855,7 +855,7 @@ describe("MollifierStaleSweepState β€” direct unit tests", () => { } finally { await state.close(); } - }, + } ); }); @@ -940,7 +940,7 @@ describe("startStaleSweepInterval β€” lifecycle", () => { reportStaleEntrySnapshot: () => {}, logger: { warn: () => {} }, now: () => Date.now(), - }, + } ); // Wait for the interval to fire one tick. The tick will start, call @@ -969,8 +969,6 @@ describe("startStaleSweepInterval β€” lifecycle", () => { // The tick's readCursor:end MUST appear before the close β€” otherwise // we closed the Redis client out from under an in-flight tick. expect(callOrder.indexOf("readCursor:end")).toBeGreaterThan(-1); - expect(callOrder.indexOf("close")).toBeGreaterThan( - callOrder.indexOf("readCursor:end"), - ); + expect(callOrder.indexOf("close")).toBeGreaterThan(callOrder.indexOf("readCursor:end")); }); }); diff --git a/apps/webapp/test/mollifierSynthesiseFoundRun.test.ts b/apps/webapp/test/mollifierSynthesiseFoundRun.test.ts index 4e2d6a6163..5c175e8889 100644 --- a/apps/webapp/test/mollifierSynthesiseFoundRun.test.ts +++ b/apps/webapp/test/mollifierSynthesiseFoundRun.test.ts @@ -120,9 +120,7 @@ describe("synthesiseFoundRunFromBuffer", () => { }); it("passes through an explicit workerQueue from the snapshot unchanged", () => { - const found = synthesiseFoundRunFromBuffer( - makeSyntheticRun({ workerQueue: "us-east-1" }) - ); + const found = synthesiseFoundRunFromBuffer(makeSyntheticRun({ workerQueue: "us-east-1" })); expect(found.workerQueue).toBe("us-east-1"); }); diff --git a/apps/webapp/test/mollifierSyntheticApiResponses.test.ts b/apps/webapp/test/mollifierSyntheticApiResponses.test.ts index 94ee67c858..37be1dfac7 100644 --- a/apps/webapp/test/mollifierSyntheticApiResponses.test.ts +++ b/apps/webapp/test/mollifierSyntheticApiResponses.test.ts @@ -77,9 +77,7 @@ describe("buildSyntheticSpanDetailBody", () => { }); it("defaults message to '' when the buffered run has no taskIdentifier", () => { - const body = buildSyntheticSpanDetailBody( - makeSyntheticRun({ taskIdentifier: undefined }) - ); + const body = buildSyntheticSpanDetailBody(makeSyntheticRun({ taskIdentifier: undefined })); expect(body.message).toBe(""); }); diff --git a/apps/webapp/test/mollifierSyntheticRedirectInfo.test.ts b/apps/webapp/test/mollifierSyntheticRedirectInfo.test.ts index a996b9de69..4ad1b6efe2 100644 --- a/apps/webapp/test/mollifierSyntheticRedirectInfo.test.ts +++ b/apps/webapp/test/mollifierSyntheticRedirectInfo.test.ts @@ -22,36 +22,39 @@ function fakePrisma(member: { id: string } | null) { } describe("findBufferedRunRedirectInfo (testcontainers)", () => { - redisTest("returns slugs + spanId for a real buffer entry when user is a member", async ({ redisOptions }) => { - const buffer = new MollifierBuffer({ redisOptions }); - try { - await buffer.accept({ - runId: "run_real_1", - envId: "env_a", - orgId: "org_1", - payload: JSON.stringify(SNAPSHOT), - }); - const info = await findBufferedRunRedirectInfo( - { runFriendlyId: "run_real_1", userId: "user_1" }, - { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) }, - ); - expect(info).toEqual({ - organizationSlug: "references-6120", - projectSlug: "hello-world-bN7m", - environmentSlug: "dev", - spanId: "span_1", - }); - } finally { - await buffer.close(); + redisTest( + "returns slugs + spanId for a real buffer entry when user is a member", + async ({ redisOptions }) => { + const buffer = new MollifierBuffer({ redisOptions }); + try { + await buffer.accept({ + runId: "run_real_1", + envId: "env_a", + orgId: "org_1", + payload: JSON.stringify(SNAPSHOT), + }); + const info = await findBufferedRunRedirectInfo( + { runFriendlyId: "run_real_1", userId: "user_1" }, + { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) } + ); + expect(info).toEqual({ + organizationSlug: "references-6120", + projectSlug: "hello-world-bN7m", + environmentSlug: "dev", + spanId: "span_1", + }); + } finally { + await buffer.close(); + } } - }); + ); redisTest("returns null when no buffer entry exists for the runId", async ({ redisOptions }) => { const buffer = new MollifierBuffer({ redisOptions }); try { const info = await findBufferedRunRedirectInfo( { runFriendlyId: "run_missing", userId: "user_1" }, - { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) }, + { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) } ); expect(info).toBeNull(); } finally { @@ -59,48 +62,56 @@ describe("findBufferedRunRedirectInfo (testcontainers)", () => { } }); - redisTest("returns null when the user is not an org member (default check enforced)", async ({ redisOptions }) => { - const buffer = new MollifierBuffer({ redisOptions }); - try { - await buffer.accept({ - runId: "run_real_2", - envId: "env_a", - orgId: "org_1", - payload: JSON.stringify(SNAPSHOT), - }); - const info = await findBufferedRunRedirectInfo( - { runFriendlyId: "run_real_2", userId: "user_other" }, - { getBuffer: () => buffer, prismaClient: fakePrisma(null) }, - ); - expect(info).toBeNull(); - } finally { - await buffer.close(); + redisTest( + "returns null when the user is not an org member (default check enforced)", + async ({ redisOptions }) => { + const buffer = new MollifierBuffer({ redisOptions }); + try { + await buffer.accept({ + runId: "run_real_2", + envId: "env_a", + orgId: "org_1", + payload: JSON.stringify(SNAPSHOT), + }); + const info = await findBufferedRunRedirectInfo( + { runFriendlyId: "run_real_2", userId: "user_other" }, + { getBuffer: () => buffer, prismaClient: fakePrisma(null) } + ); + expect(info).toBeNull(); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("skips the org-membership check when skipOrgMembershipCheck is set (admin path)", async ({ redisOptions }) => { - const buffer = new MollifierBuffer({ redisOptions }); - try { - await buffer.accept({ - runId: "run_real_3", - envId: "env_a", - orgId: "org_1", - payload: JSON.stringify(SNAPSHOT), - }); - const findFirst = vi.fn(); - const info = await findBufferedRunRedirectInfo( - { runFriendlyId: "run_real_3", userId: "user_admin", skipOrgMembershipCheck: true }, - { - getBuffer: () => buffer, - prismaClient: { orgMember: { findFirst } } as unknown as Parameters[1]["prismaClient"], - }, - ); - expect(info?.organizationSlug).toBe("references-6120"); - expect(findFirst).not.toHaveBeenCalled(); - } finally { - await buffer.close(); + redisTest( + "skips the org-membership check when skipOrgMembershipCheck is set (admin path)", + async ({ redisOptions }) => { + const buffer = new MollifierBuffer({ redisOptions }); + try { + await buffer.accept({ + runId: "run_real_3", + envId: "env_a", + orgId: "org_1", + payload: JSON.stringify(SNAPSHOT), + }); + const findFirst = vi.fn(); + const info = await findBufferedRunRedirectInfo( + { runFriendlyId: "run_real_3", userId: "user_admin", skipOrgMembershipCheck: true }, + { + getBuffer: () => buffer, + prismaClient: { orgMember: { findFirst } } as unknown as Parameters< + typeof findBufferedRunRedirectInfo + >[1]["prismaClient"], + } + ); + expect(info?.organizationSlug).toBe("references-6120"); + expect(findFirst).not.toHaveBeenCalled(); + } finally { + await buffer.close(); + } } - }); + ); redisTest("returns null when snapshot is malformed JSON", async ({ redisOptions }) => { const buffer = new MollifierBuffer({ redisOptions }); @@ -113,7 +124,7 @@ describe("findBufferedRunRedirectInfo (testcontainers)", () => { }); const info = await findBufferedRunRedirectInfo( { runFriendlyId: "run_real_4", userId: "user_1" }, - { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) }, + { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) } ); expect(info).toBeNull(); } finally { @@ -132,7 +143,7 @@ describe("findBufferedRunRedirectInfo (testcontainers)", () => { }); const info = await findBufferedRunRedirectInfo( { runFriendlyId: "run_real_5", userId: "user_1" }, - { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) }, + { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) } ); expect(info).toBeNull(); } finally { @@ -140,25 +151,28 @@ describe("findBufferedRunRedirectInfo (testcontainers)", () => { } }); - redisTest("returns info with undefined spanId when snapshot has no spanId", async ({ redisOptions }) => { - const buffer = new MollifierBuffer({ redisOptions }); - try { - await buffer.accept({ - runId: "run_real_6", - envId: "env_a", - orgId: "org_1", - payload: JSON.stringify({ environment: SNAPSHOT.environment }), - }); - const info = await findBufferedRunRedirectInfo( - { runFriendlyId: "run_real_6", userId: "user_1" }, - { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) }, - ); - expect(info?.spanId).toBeUndefined(); - expect(info?.environmentSlug).toBe("dev"); - } finally { - await buffer.close(); + redisTest( + "returns info with undefined spanId when snapshot has no spanId", + async ({ redisOptions }) => { + const buffer = new MollifierBuffer({ redisOptions }); + try { + await buffer.accept({ + runId: "run_real_6", + envId: "env_a", + orgId: "org_1", + payload: JSON.stringify({ environment: SNAPSHOT.environment }), + }); + const info = await findBufferedRunRedirectInfo( + { runFriendlyId: "run_real_6", userId: "user_1" }, + { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) } + ); + expect(info?.spanId).toBeUndefined(); + expect(info?.environmentSlug).toBe("dev"); + } finally { + await buffer.close(); + } } - }); + ); redisTest( "rejects snapshots where a slug is the wrong type (Zod guard, not just typeof)", @@ -186,12 +200,12 @@ describe("findBufferedRunRedirectInfo (testcontainers)", () => { }); const info = await findBufferedRunRedirectInfo( { runFriendlyId: "run_real_7", userId: "user_1" }, - { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) }, + { getBuffer: () => buffer, prismaClient: fakePrisma({ id: "member_1" }) } ); expect(info).toBeNull(); } finally { await buffer.close(); } - }, + } ); }); diff --git a/apps/webapp/test/mollifierSyntheticTrace.test.ts b/apps/webapp/test/mollifierSyntheticTrace.test.ts index e711eb0ffe..69ce8111e4 100644 --- a/apps/webapp/test/mollifierSyntheticTrace.test.ts +++ b/apps/webapp/test/mollifierSyntheticTrace.test.ts @@ -78,9 +78,7 @@ describe("buildSyntheticTraceForBufferedRun", () => { }); it("falls back to an empty-string span id when the snapshot has no spanId", () => { - const trace = buildSyntheticTraceForBufferedRun( - makeSyntheticRun({ spanId: undefined }) - ); + const trace = buildSyntheticTraceForBufferedRun(makeSyntheticRun({ spanId: undefined })); expect(trace.events[0].id).toBe(""); // Empty id still marks as root (it matches the rootId fallback). expect(trace.events[0].data.isRoot).toBe(true); diff --git a/apps/webapp/test/mollifierTripEvaluator.test.ts b/apps/webapp/test/mollifierTripEvaluator.test.ts index 14ac0cc55b..0009233242 100644 --- a/apps/webapp/test/mollifierTripEvaluator.test.ts +++ b/apps/webapp/test/mollifierTripEvaluator.test.ts @@ -26,7 +26,7 @@ describe("createRealTripEvaluator", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -57,7 +57,7 @@ describe("createRealTripEvaluator", () => { } finally { await buffer.close(); } - }, + } ); redisTest("returns divert=false when getBuffer returns null (fail-open)", async () => { @@ -70,21 +70,18 @@ describe("createRealTripEvaluator", () => { expect(decision).toEqual({ divert: false }); }); - redisTest( - "returns divert=false when buffer throws (fail-open)", - async ({ redisOptions }) => { - const buffer = new MollifierBuffer({ redisOptions }); - // Closing the client up front means evaluateTrip will throw on the first - // Redis command β€” a real failure mode, not a stub. - await buffer.close(); + redisTest("returns divert=false when buffer throws (fail-open)", async ({ redisOptions }) => { + const buffer = new MollifierBuffer({ redisOptions }); + // Closing the client up front means evaluateTrip will throw on the first + // Redis command β€” a real failure mode, not a stub. + await buffer.close(); - const evaluator = createRealTripEvaluator({ - getBuffer: () => buffer, - options: () => ({ windowMs: 200, threshold: 100, holdMs: 500 }), - }); + const evaluator = createRealTripEvaluator({ + getBuffer: () => buffer, + options: () => ({ windowMs: 200, threshold: 100, holdMs: 500 }), + }); - const decision = await evaluator(inputs); - expect(decision).toEqual({ divert: false }); - }, - ); + const decision = await evaluator(inputs); + expect(decision).toEqual({ divert: false }); + }); }); diff --git a/apps/webapp/test/objectStore.test.ts b/apps/webapp/test/objectStore.test.ts index bdfd1f7cfb..617e6b08b9 100644 --- a/apps/webapp/test/objectStore.test.ts +++ b/apps/webapp/test/objectStore.test.ts @@ -181,7 +181,9 @@ describe("Object Storage", () => { const key = `packets/proj_ref/dev/${path}`; const normalized = normalizeObjectStoreLogicalKeyPathname(key); expect(normalized).toBe(`/packets/proj_ref/dev/${path}`); - expect(() => assertPacketObjectStoreKeyUnderPrefix(key, "packets/proj_ref/dev")).not.toThrow(); + expect(() => + assertPacketObjectStoreKeyUnderPrefix(key, "packets/proj_ref/dev") + ).not.toThrow(); }); }); @@ -206,7 +208,9 @@ describe("Object Storage", () => { it("rejects env-level traversal via encoded parent segments", () => { const traversalKey = `${prefix}/%2e%2e/secret.json`; - expect(normalizeObjectStoreLogicalKeyPathname(traversalKey)).toBe("/packets/proj_ref/secret.json"); + expect(normalizeObjectStoreLogicalKeyPathname(traversalKey)).toBe( + "/packets/proj_ref/secret.json" + ); expect(() => assertPacketObjectStoreKeyUnderPrefix(traversalKey, prefix)).toThrow( ServiceValidationError @@ -217,9 +221,7 @@ describe("Object Storage", () => { describe("normalizeObjectStoreLogicalKeyPathname (Aws4FetchClient behavior)", () => { it("decodes %2e%2e segments into parent directory traversal", () => { const key = "packets/proj_ref/dev/run/%2e%2e/secret.json"; - expect(normalizeObjectStoreLogicalKeyPathname(key)).toBe( - "/packets/proj_ref/dev/secret.json" - ); + expect(normalizeObjectStoreLogicalKeyPathname(key)).toBe("/packets/proj_ref/dev/secret.json"); }); }); @@ -241,7 +243,8 @@ describe("Object Storage", () => { }); expect(response.status).toBe(500); expect(await response.json()).toEqual({ - error: "Failed to generate presigned URL: Object store is not configured for protocol: default", + error: + "Failed to generate presigned URL: Object store is not configured for protocol: default", }); }); @@ -265,16 +268,13 @@ describe("Object Storage", () => { "run/%2e%2e/secret.json", "%2e%2e/secret.json", "%2E%2E/secret.json", - ])( - "returns 400 failure for unsafe path %s without calling object store", - async (filename) => { - const result = await generatePresignedUrl("proj_test", "dev", filename, "PUT"); - expect(result.success).toBe(false); - if (result.success) throw new Error("expected presign to fail"); - expect(result.error).toBe(INVALID_PACKET_STORAGE_PATH); - expect(result.status).toBe(400); - } - ); + ])("returns 400 failure for unsafe path %s without calling object store", async (filename) => { + const result = await generatePresignedUrl("proj_test", "dev", filename, "PUT"); + expect(result.success).toBe(false); + if (result.success) throw new Error("expected presign to fail"); + expect(result.error).toBe(INVALID_PACKET_STORAGE_PATH); + expect(result.status).toBe(400); + }); it("allows presign for valid packet paths when object store is not configured", async () => { env.OBJECT_STORE_BASE_URL = undefined; @@ -309,7 +309,9 @@ describe("Object Storage", () => { it("PUT with forceNoPrefix skips OBJECT_STORE_DEFAULT_PROTOCOL for unprefixed keys", () => { env.OBJECT_STORE_DEFAULT_PROTOCOL = "s3"; - expect(resolveStoreProtocolForPacketPresign("a/b.json", "PUT", true).storeProtocol).toBeUndefined(); + expect( + resolveStoreProtocolForPacketPresign("a/b.json", "PUT", true).storeProtocol + ).toBeUndefined(); }); it("explicit protocol in key wins for PUT with forceNoPrefix", () => { @@ -687,7 +689,12 @@ describe("Object Storage", () => { }); expect(putResponse.ok).toBe(true); - const getResult = await generatePresignedUrl(projectRef, envSlug, putResult.storagePath!, "GET"); + const getResult = await generatePresignedUrl( + projectRef, + envSlug, + putResult.storagePath!, + "GET" + ); expect(getResult.success).toBe(true); if (!getResult.success) throw new Error(getResult.error); diff --git a/apps/webapp/test/organizationDataStoresRegistry.test.ts b/apps/webapp/test/organizationDataStoresRegistry.test.ts index 9d94e0447f..87fdeced32 100644 --- a/apps/webapp/test/organizationDataStoresRegistry.test.ts +++ b/apps/webapp/test/organizationDataStoresRegistry.test.ts @@ -62,23 +62,26 @@ describe("OrganizationDataStoresRegistry", () => { expect(secret).not.toBeNull(); }); - postgresTest("loadFromDatabase resolves secrets and makes orgs available via get", async ({ prisma }) => { - const registry = new OrganizationDataStoresRegistry(prisma, prisma); + postgresTest( + "loadFromDatabase resolves secrets and makes orgs available via get", + async ({ prisma }) => { + const registry = new OrganizationDataStoresRegistry(prisma, prisma); - await registry.addDataStore({ - key: "hipaa-store", - kind: "CLICKHOUSE", - organizationIds: ["org-hipaa"], - config: ClickhouseConnectionSchema.parse({ url: TEST_URL }), - }); + await registry.addDataStore({ + key: "hipaa-store", + kind: "CLICKHOUSE", + organizationIds: ["org-hipaa"], + config: ClickhouseConnectionSchema.parse({ url: TEST_URL }), + }); - await registry.loadFromDatabase(); + await registry.loadFromDatabase(); - const result = registry.get("org-hipaa", "CLICKHOUSE"); - expect(result).not.toBeNull(); - expect(result?.kind).toBe("CLICKHOUSE"); - expect(result?.url).toBe(TEST_URL); - }); + const result = registry.get("org-hipaa", "CLICKHOUSE"); + expect(result).not.toBeNull(); + expect(result?.kind).toBe("CLICKHOUSE"); + expect(result?.url).toBe(TEST_URL); + } + ); postgresTest("get returns null for orgs not in any data store", async ({ prisma }) => { const registry = new OrganizationDataStoresRegistry(prisma, prisma); @@ -144,36 +147,38 @@ describe("OrganizationDataStoresRegistry", () => { await registry.loadFromDatabase(); - const expectedUrl = - winner!.key === "dup-overlap-first" ? TEST_URL : TEST_URL_2; + const expectedUrl = winner!.key === "dup-overlap-first" ? TEST_URL : TEST_URL_2; expect(registry.get(sharedOrg, "CLICKHOUSE")?.url).toBe(expectedUrl); } ); - postgresTest("updateDataStore updates organizationIds and rotates the secret", async ({ prisma }) => { - const registry = new OrganizationDataStoresRegistry(prisma, prisma); + postgresTest( + "updateDataStore updates organizationIds and rotates the secret", + async ({ prisma }) => { + const registry = new OrganizationDataStoresRegistry(prisma, prisma); - await registry.addDataStore({ - key: "update-store", - kind: "CLICKHOUSE", - organizationIds: ["org-old"], - config: ClickhouseConnectionSchema.parse({ url: TEST_URL }), - }); + await registry.addDataStore({ + key: "update-store", + kind: "CLICKHOUSE", + organizationIds: ["org-old"], + config: ClickhouseConnectionSchema.parse({ url: TEST_URL }), + }); - await registry.updateDataStore({ - key: "update-store", - kind: "CLICKHOUSE", - organizationIds: ["org-new-1", "org-new-2"], - config: ClickhouseConnectionSchema.parse({ url: TEST_URL_2 }), - }); + await registry.updateDataStore({ + key: "update-store", + kind: "CLICKHOUSE", + organizationIds: ["org-new-1", "org-new-2"], + config: ClickhouseConnectionSchema.parse({ url: TEST_URL_2 }), + }); - const row = await prisma.organizationDataStore.findFirst({ where: { key: "update-store" } }); - expect(row?.organizationIds).toEqual(["org-new-1", "org-new-2"]); + const row = await prisma.organizationDataStore.findFirst({ where: { key: "update-store" } }); + expect(row?.organizationIds).toEqual(["org-new-1", "org-new-2"]); - await registry.loadFromDatabase(); - expect(registry.get("org-new-1", "CLICKHOUSE")?.url).toBe(TEST_URL_2); - expect(registry.get("org-old", "CLICKHOUSE")).toBeNull(); - }); + await registry.loadFromDatabase(); + expect(registry.get("org-new-1", "CLICKHOUSE")?.url).toBe(TEST_URL_2); + expect(registry.get("org-old", "CLICKHOUSE")).toBeNull(); + } + ); postgresTest("reload picks up changes made after initial load", async ({ prisma }) => { const registry = new OrganizationDataStoresRegistry(prisma, prisma); @@ -205,8 +210,12 @@ describe("OrganizationDataStoresRegistry", () => { await registry.deleteDataStore({ key: "delete-store", kind: "CLICKHOUSE" }); - expect(await prisma.organizationDataStore.findFirst({ where: { key: "delete-store" } })).toBeNull(); - expect(await prisma.secretStore.findFirst({ where: { key: "data-store:delete-store:clickhouse" } })).toBeNull(); + expect( + await prisma.organizationDataStore.findFirst({ where: { key: "delete-store" } }) + ).toBeNull(); + expect( + await prisma.secretStore.findFirst({ where: { key: "data-store:delete-store:clickhouse" } }) + ).toBeNull(); }); postgresTest("after delete and reload, org no longer has a data store", async ({ prisma }) => { diff --git a/apps/webapp/test/presenters/ApiBatchResultsPresenter.test.ts b/apps/webapp/test/presenters/ApiBatchResultsPresenter.test.ts index d0888ba6a1..93c4fc59d4 100644 --- a/apps/webapp/test/presenters/ApiBatchResultsPresenter.test.ts +++ b/apps/webapp/test/presenters/ApiBatchResultsPresenter.test.ts @@ -1,5 +1,10 @@ import { containerTest } from "@internal/testcontainers"; -import type { Organization, PrismaClient, Project, RuntimeEnvironment } from "@trigger.dev/database"; +import type { + Organization, + PrismaClient, + Project, + RuntimeEnvironment, +} from "@trigger.dev/database"; import { customAlphabet } from "nanoid"; import { expect, vi } from "vitest"; import { ApiBatchResultsPresenter } from "~/presenters/v3/ApiBatchResultsPresenter.server"; @@ -31,7 +36,10 @@ type SeedContext = { queueId: string; }; -async function seedWorker(prisma: PrismaClient, ctx: Omit) { +async function seedWorker( + prisma: PrismaClient, + ctx: Omit +) { const queue = await prisma.taskQueue.create({ data: { friendlyId: `queue_${idGenerator()}`, @@ -136,7 +144,7 @@ containerTest( // A successful run, a failed run, and an executing run (no terminal attempt β†’ undefined). const successRun = await seedRunWithAttempt(prisma, ctx, { status: "COMPLETED_SUCCESSFULLY", - attempt: { status: "COMPLETED", output: "\"hello\"", outputType: "application/json" }, + attempt: { status: "COMPLETED", output: '"hello"', outputType: "application/json" }, }); const failedRun = await seedRunWithAttempt(prisma, ctx, { status: "COMPLETED_WITH_ERRORS", @@ -170,7 +178,10 @@ containerTest( } const presenter = new ApiBatchResultsPresenter(prisma); - const result = await presenter.call(batchFriendlyId, authEnv(environment, project, organization)); + const result = await presenter.call( + batchFriendlyId, + authEnv(environment, project, organization) + ); expect(result).toBeDefined(); expect(result?.id).toBe(batchFriendlyId); @@ -182,7 +193,7 @@ containerTest( expect(first.ok).toBe(true); expect(first.id).toBe(successRun.friendlyId); if (first.ok) { - expect(first.output).toBe("\"hello\""); + expect(first.output).toBe('"hello"'); expect(first.taskIdentifier).toBe("test-task"); } @@ -212,7 +223,7 @@ containerTest( const pendingRun = await seedRunWithAttempt(prisma, ctx, { status: "EXECUTING" }); const successRun = await seedRunWithAttempt(prisma, ctx, { status: "COMPLETED_SUCCESSFULLY", - attempt: { status: "COMPLETED", output: "\"ok\"", outputType: "application/json" }, + attempt: { status: "COMPLETED", output: '"ok"', outputType: "application/json" }, }); const batchInternalId = idGenerator(); @@ -233,7 +244,10 @@ containerTest( } const presenter = new ApiBatchResultsPresenter(prisma); - const result = await presenter.call(batchFriendlyId, authEnv(environment, project, organization)); + const result = await presenter.call( + batchFriendlyId, + authEnv(environment, project, organization) + ); expect(result?.items).toHaveLength(1); expect(result?.items[0]?.id).toBe(successRun.friendlyId); diff --git a/apps/webapp/test/prismaInfrastructureErrorCapture.test.ts b/apps/webapp/test/prismaInfrastructureErrorCapture.test.ts index 63170c9e28..892befb97f 100644 --- a/apps/webapp/test/prismaInfrastructureErrorCapture.test.ts +++ b/apps/webapp/test/prismaInfrastructureErrorCapture.test.ts @@ -21,64 +21,65 @@ function capturingLogger() { } describe("captureInfrastructureErrors", () => { - postgresTest("P2025 (not found) passes through with code intact and unlogged", async ({ - prisma, - }) => { - const log = capturingLogger(); - const client = captureInfrastructureErrors(prisma, log); - - const error = await client.secretStore - .update({ where: { key: "does-not-exist" }, data: { version: "2" } }) - .then(() => undefined) - .catch((e) => e); - - expect(error).toBeInstanceOf(Prisma.PrismaClientKnownRequestError); - expect((error as Prisma.PrismaClientKnownRequestError).code).toBe("P2025"); - expect(log.captured).toHaveLength(0); - }); + postgresTest( + "P2025 (not found) passes through with code intact and unlogged", + async ({ prisma }) => { + const log = capturingLogger(); + const client = captureInfrastructureErrors(prisma, log); - postgresTest("P2002 (unique violation) passes through with code intact and unlogged", async ({ - prisma, - }) => { - const log = capturingLogger(); - const client = captureInfrastructureErrors(prisma, log); + const error = await client.secretStore + .update({ where: { key: "does-not-exist" }, data: { version: "2" } }) + .then(() => undefined) + .catch((e) => e); - await client.secretStore.create({ data: { key: "dup-key", value: { a: 1 } } }); + expect(error).toBeInstanceOf(Prisma.PrismaClientKnownRequestError); + expect((error as Prisma.PrismaClientKnownRequestError).code).toBe("P2025"); + expect(log.captured).toHaveLength(0); + } + ); - const error = await client.secretStore - .create({ data: { key: "dup-key", value: { a: 2 } } }) - .then(() => undefined) - .catch((e) => e); + postgresTest( + "P2002 (unique violation) passes through with code intact and unlogged", + async ({ prisma }) => { + const log = capturingLogger(); + const client = captureInfrastructureErrors(prisma, log); - expect(error).toBeInstanceOf(Prisma.PrismaClientKnownRequestError); - expect((error as Prisma.PrismaClientKnownRequestError).code).toBe("P2002"); - expect(log.captured).toHaveLength(0); - }); + await client.secretStore.create({ data: { key: "dup-key", value: { a: 1 } } }); - postgresTest("errors raised inside an interactive $transaction keep their code", async ({ - prisma, - }) => { - const log = capturingLogger(); - const client = captureInfrastructureErrors(prisma, log); + const error = await client.secretStore + .create({ data: { key: "dup-key", value: { a: 2 } } }) + .then(() => undefined) + .catch((e) => e); - // Proves $allOperations fires per-statement inside a transaction β€” the - // basis for transaction retry logic (which branches on error.code) staying - // intact. - const error = await client - .$transaction(async (tx) => { - await tx.secretStore.update({ where: { key: "missing-in-tx" }, data: { version: "2" } }); - }) - .then(() => undefined) - .catch((e) => e); + expect(error).toBeInstanceOf(Prisma.PrismaClientKnownRequestError); + expect((error as Prisma.PrismaClientKnownRequestError).code).toBe("P2002"); + expect(log.captured).toHaveLength(0); + } + ); + + postgresTest( + "errors raised inside an interactive $transaction keep their code", + async ({ prisma }) => { + const log = capturingLogger(); + const client = captureInfrastructureErrors(prisma, log); + + // Proves $allOperations fires per-statement inside a transaction β€” the + // basis for transaction retry logic (which branches on error.code) staying + // intact. + const error = await client + .$transaction(async (tx) => { + await tx.secretStore.update({ where: { key: "missing-in-tx" }, data: { version: "2" } }); + }) + .then(() => undefined) + .catch((e) => e); - expect(error).toBeInstanceOf(Prisma.PrismaClientKnownRequestError); - expect((error as Prisma.PrismaClientKnownRequestError).code).toBe("P2025"); - expect(log.captured).toHaveLength(0); - }); + expect(error).toBeInstanceOf(Prisma.PrismaClientKnownRequestError); + expect((error as Prisma.PrismaClientKnownRequestError).code).toBe("P2025"); + expect(log.captured).toHaveLength(0); + } + ); - postgresTest("raw queries (model undefined) are wrapped without crashing", async ({ - prisma, - }) => { + postgresTest("raw queries (model undefined) are wrapped without crashing", async ({ prisma }) => { const log = capturingLogger(); const client = captureInfrastructureErrors(prisma, log); diff --git a/apps/webapp/test/rbacFallbackBranch.test.ts b/apps/webapp/test/rbacFallbackBranch.test.ts index cb19f610d9..f8e90e444c 100644 --- a/apps/webapp/test/rbacFallbackBranch.test.ts +++ b/apps/webapp/test/rbacFallbackBranch.test.ts @@ -82,27 +82,30 @@ describe("RBAC fallback β€” DEVELOPMENT branch pivot", () => { expect(result.environment.apiKey).toBe(devRoot.apiKey); }); - postgresTest("the 'default' sentinel resolves the root dev env (no pivot)", async ({ prisma }) => { - const { organization, project, orgMember } = await createTestOrgProjectWithMember(prisma); - const rbac = makeController(prisma); - - const devRoot = await createEnv(prisma, project.id, organization.id, { - type: "DEVELOPMENT", - orgMemberId: orgMember.id, - }); - await createEnv(prisma, project.id, organization.id, { - type: "DEVELOPMENT", - orgMemberId: orgMember.id, - parentEnvironmentId: devRoot.id, - branchName: "my-feature", - }); - - const result = await rbac.authenticateBearer(bearerRequest(devRoot.apiKey, "default")); - - expect(result.ok).toBe(true); - if (!result.ok) return; - expect(result.environment.id).toBe(devRoot.id); - }); + postgresTest( + "the 'default' sentinel resolves the root dev env (no pivot)", + async ({ prisma }) => { + const { organization, project, orgMember } = await createTestOrgProjectWithMember(prisma); + const rbac = makeController(prisma); + + const devRoot = await createEnv(prisma, project.id, organization.id, { + type: "DEVELOPMENT", + orgMemberId: orgMember.id, + }); + await createEnv(prisma, project.id, organization.id, { + type: "DEVELOPMENT", + orgMemberId: orgMember.id, + parentEnvironmentId: devRoot.id, + branchName: "my-feature", + }); + + const result = await rbac.authenticateBearer(bearerRequest(devRoot.apiKey, "default")); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.environment.id).toBe(devRoot.id); + } + ); postgresTest("no branch header resolves the root dev env", async ({ prisma }) => { const { organization, project, orgMember } = await createTestOrgProjectWithMember(prisma); @@ -120,23 +123,24 @@ describe("RBAC fallback β€” DEVELOPMENT branch pivot", () => { expect(result.environment.id).toBe(devRoot.id); }); - postgresTest("a named branch that doesn't exist is rejected (not a fall-through)", async ({ - prisma, - }) => { - const { organization, project, orgMember } = await createTestOrgProjectWithMember(prisma); - const rbac = makeController(prisma); + postgresTest( + "a named branch that doesn't exist is rejected (not a fall-through)", + async ({ prisma }) => { + const { organization, project, orgMember } = await createTestOrgProjectWithMember(prisma); + const rbac = makeController(prisma); - const devRoot = await createEnv(prisma, project.id, organization.id, { - type: "DEVELOPMENT", - orgMemberId: orgMember.id, - }); + const devRoot = await createEnv(prisma, project.id, organization.id, { + type: "DEVELOPMENT", + orgMemberId: orgMember.id, + }); - const result = await rbac.authenticateBearer(bearerRequest(devRoot.apiKey, "nope")); + const result = await rbac.authenticateBearer(bearerRequest(devRoot.apiKey, "nope")); - expect(result.ok).toBe(false); - if (result.ok) return; - expect(result.status).toBe(401); - }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.status).toBe(401); + } + ); }); describe("RBAC fallback β€” branch header guards", () => { @@ -145,29 +149,30 @@ describe("RBAC fallback β€” branch header guards", () => { // PREVIEW branch literally named "default" is reachable and the request pivots // to it like any other branch. (Preview branch names are normally PR refs, so // a branch named "default" is unusual β€” but it's supported, not a collision.) - postgresTest("preview + 'default' pivots to the branch named 'default' (sentinel is dev-only)", async ({ - prisma, - }) => { - const { organization, project } = await createTestOrgProjectWithMember(prisma); - const rbac = makeController(prisma); - - const previewParent = await createEnv(prisma, project.id, organization.id, { - type: "PREVIEW", - isBranchableEnvironment: true, - }); - const previewDefaultBranch = await createEnv(prisma, project.id, organization.id, { - type: "PREVIEW", - parentEnvironmentId: previewParent.id, - branchName: "default", - }); - - const result = await rbac.authenticateBearer(bearerRequest(previewParent.apiKey, "default")); - - expect(result.ok).toBe(true); - if (!result.ok) return; - // Pivots to the branch named "default", carrying the parent's api key. - expect(result.environment.id).toBe(previewDefaultBranch.id); - expect(result.environment.id).not.toBe(previewParent.id); - expect(result.environment.apiKey).toBe(previewParent.apiKey); - }); + postgresTest( + "preview + 'default' pivots to the branch named 'default' (sentinel is dev-only)", + async ({ prisma }) => { + const { organization, project } = await createTestOrgProjectWithMember(prisma); + const rbac = makeController(prisma); + + const previewParent = await createEnv(prisma, project.id, organization.id, { + type: "PREVIEW", + isBranchableEnvironment: true, + }); + const previewDefaultBranch = await createEnv(prisma, project.id, organization.id, { + type: "PREVIEW", + parentEnvironmentId: previewParent.id, + branchName: "default", + }); + + const result = await rbac.authenticateBearer(bearerRequest(previewParent.apiKey, "default")); + + expect(result.ok).toBe(true); + if (!result.ok) return; + // Pivots to the branch named "default", carrying the parent's api key. + expect(result.environment.id).toBe(previewDefaultBranch.id); + expect(result.environment.id).not.toBe(previewParent.id); + expect(result.environment.apiKey).toBe(previewParent.apiKey); + } + ); }); diff --git a/apps/webapp/test/realtime/envChangeRouter.test.ts b/apps/webapp/test/realtime/envChangeRouter.test.ts index 022b5827cd..123146dd12 100644 --- a/apps/webapp/test/realtime/envChangeRouter.test.ts +++ b/apps/webapp/test/realtime/envChangeRouter.test.ts @@ -166,10 +166,7 @@ describe("EnvChangeRouter", () => { const { router, src } = makeRouter(rows); const reg = router.register("env_1", { kind: "batch", batchId: "batch_1" }, []); const wait = reg.waitForMatch(undefined, 1_000); - src.push("env_1", [ - record("rX", { batchId: "other" }), - record("r1", { batchId: "batch_1" }), - ]); + src.push("env_1", [record("rX", { batchId: "other" }), record("r1", { batchId: "batch_1" })]); const result = await wait; expect(result.rows.map((m) => m.row.id)).toEqual(["r1"]); reg.close(); @@ -186,7 +183,10 @@ describe("EnvChangeRouter", () => { // r_one shares a tag (routes as a candidate via the index) but lacks "b" β€” must be // culled by the authoritative row check. r_both carries both and wakes the feed. - src.push("env_1", [record("r_one", { tags: ["a"] }), record("r_both", { tags: ["a", "b", "c"] })]); + src.push("env_1", [ + record("r_one", { tags: ["a"] }), + record("r_both", { tags: ["a", "b", "c"] }), + ]); const result = await wait; expect(result.reason).toBe("notify"); @@ -197,7 +197,11 @@ describe("EnvChangeRouter", () => { it("drops a tag match created before the feed's createdAt floor", async () => { const rows = new Map([["r1", row("r1", { tags: ["a"], createdAtMs: FLOOR_MS - 10_000 })]]); const { router, src } = makeRouter(rows); - const reg = router.register("env_1", { kind: "tag", tags: ["a"], createdAtFloorMs: FLOOR_MS }, []); + const reg = router.register( + "env_1", + { kind: "tag", tags: ["a"], createdAtFloorMs: FLOOR_MS }, + [] + ); let settled = false; const wait = reg.waitForMatch(undefined, 60).then((r) => { settled = true; @@ -375,8 +379,7 @@ describe("EnvChangeRouter read-your-writes gate", () => { marginMs: 0, maxDelayMs: 1_000, staleRetries: 3, - onStaleHydrate: (outcome: string, runCount: number) => - outcomes.push({ outcome, runCount }), + onStaleHydrate: (outcome: string, runCount: number) => outcomes.push({ outcome, runCount }), ...overrides, }, }; diff --git a/apps/webapp/test/realtime/nativeHoldOnEmpty.test.ts b/apps/webapp/test/realtime/nativeHoldOnEmpty.test.ts index 615abc9039..98d21f870a 100644 --- a/apps/webapp/test/realtime/nativeHoldOnEmpty.test.ts +++ b/apps/webapp/test/realtime/nativeHoldOnEmpty.test.ts @@ -5,10 +5,7 @@ import { type RealtimeListEnvironment, } from "~/services/realtime/nativeRealtimeClient.server"; import { type RealtimeRunRow } from "~/services/realtime/electricStreamProtocol.server"; -import { - EnvChangeRouter, - type EnvChangeSource, -} from "~/services/realtime/envChangeRouter.server"; +import { EnvChangeRouter, type EnvChangeSource } from "~/services/realtime/envChangeRouter.server"; import { type ChangeRecord } from "~/services/realtime/runChangeNotifier.server"; import { describe, expect, it, vi } from "vitest"; @@ -70,7 +67,7 @@ function makeClient(overrides: Record = {}) { hydrator: { hydrateByIds: hydrateSpy }, replayWindowMs: 0, unsubscribeLingerMs: 0, - ...(overrides.routerOptions as Record ?? {}), + ...((overrides.routerOptions as Record) ?? {}), }); delete overrides.routerOptions; @@ -87,7 +84,13 @@ function makeClient(overrides: Record = {}) { ...overrides, }); - return { client, src, hydrateSpy, resolveSpy, setRows: (rows: RealtimeRunRow[]) => (rowsToReturn = rows) }; + return { + client, + src, + hydrateSpy, + resolveSpy, + setRows: (rows: RealtimeRunRow[]) => (rowsToReturn = rows), + }; } function liveRuns(client: NativeRealtimeClient) { @@ -163,7 +166,9 @@ describe("NativeRealtimeClient multi-run live path over the router", () => { it("a matching run created before the window floor is hydrated but dropped (keeps holding)", async () => { // Generous backstop so the "still holding" assertion can't race a timeout in slow CI. - const { client, src, hydrateSpy, resolveSpy, setRows } = makeClient({ livePollTimeoutMs: 1500 }); + const { client, src, hydrateSpy, resolveSpy, setRows } = makeClient({ + livePollTimeoutMs: 1500, + }); setRows([row("run_1", FLOOR_MS + 5_000, { createdAtMs: FLOOR_MS - 10_000, tags: ["t"] })]); const responsePromise = liveRuns(client); @@ -233,7 +238,14 @@ describe("NativeRealtimeClient multi-run live path over the router", () => { const url = `http://localhost:3030/realtime/v1/runs/run_1?offset=${FLOOR_MS + 1_000}_1&handle=run-run_1&live=true`; // First poll subscribes the env, then drains via its backstop. - const first = await client.streamRun(url, ENV, "run_1", CURRENT_API_VERSION, undefined, "1.0.0"); + const first = await client.streamRun( + url, + ENV, + "run_1", + CURRENT_API_VERSION, + undefined, + "1.0.0" + ); expect(first.status).toBe(200); // The record lands between polls; the lingering env subscription buffers it. diff --git a/apps/webapp/test/realtime/nativeRunSetCache.test.ts b/apps/webapp/test/realtime/nativeRunSetCache.test.ts index 2389fd7808..a2337ac7b8 100644 --- a/apps/webapp/test/realtime/nativeRunSetCache.test.ts +++ b/apps/webapp/test/realtime/nativeRunSetCache.test.ts @@ -176,7 +176,10 @@ describe("NativeRealtimeClient resolve admission gate (mass-reconnect stampede)" }; } - function makeGatedClient(resolveAdmissionLimit: number, resolver: ReturnType) { + function makeGatedClient( + resolveAdmissionLimit: number, + resolver: ReturnType + ) { const hydrateSpy = vi.fn(async (_env: string, ids: string[]) => ids.map(row)); return new NativeRealtimeClient({ runReader: { getRunById: async () => null, hydrateByIds: hydrateSpy } as any, diff --git a/apps/webapp/test/realtime/replayCursorStore.test.ts b/apps/webapp/test/realtime/replayCursorStore.test.ts index b66bc72df9..4a31cc62ae 100644 --- a/apps/webapp/test/realtime/replayCursorStore.test.ts +++ b/apps/webapp/test/realtime/replayCursorStore.test.ts @@ -42,24 +42,25 @@ describe("RedisReplayCursorStore", () => { } }); - redisTest("a second store instance reads the first's cursor (fleet sharing)", async ({ - redisOptions, - }) => { - const a = new RedisReplayCursorStore({ - redis: { ...redisOptions, tlsDisabled: true }, - ttlMs: 60_000, - }); - const b = new RedisReplayCursorStore({ - redis: { ...redisOptions, tlsDisabled: true }, - ttlMs: 60_000, - }); - try { - a.set("env_1:h2", 42_000); - await vi.waitFor(async () => expect(await b.get("env_1:h2")).toBe(42_000)); - } finally { - await Promise.all([a.quit(), b.quit()]); + redisTest( + "a second store instance reads the first's cursor (fleet sharing)", + async ({ redisOptions }) => { + const a = new RedisReplayCursorStore({ + redis: { ...redisOptions, tlsDisabled: true }, + ttlMs: 60_000, + }); + const b = new RedisReplayCursorStore({ + redis: { ...redisOptions, tlsDisabled: true }, + ttlMs: 60_000, + }); + try { + a.set("env_1:h2", 42_000); + await vi.waitFor(async () => expect(await b.get("env_1:h2")).toBe(42_000)); + } finally { + await Promise.all([a.quit(), b.quit()]); + } } - }); + ); it("degrades to undefined within the read deadline when Redis is unreachable", async () => { const results: Array<[string, boolean]> = []; @@ -79,7 +80,11 @@ describe("RedisReplayCursorStore", () => { }); describe("NativeRealtimeClient replay-cursor threading", () => { - const ENV: RealtimeListEnvironment = { id: "env_1", organizationId: "org_1", projectId: "proj_1" }; + const ENV: RealtimeListEnvironment = { + id: "env_1", + organizationId: "org_1", + projectId: "proj_1", + }; const FLOOR_MS = Date.UTC(2026, 5, 7, 12, 0, 0); it("passes the stored cursor to register and stamps the store after responding", async () => { diff --git a/apps/webapp/test/realtime/runChangeNotifier.test.ts b/apps/webapp/test/realtime/runChangeNotifier.test.ts index 96d7fd56a4..d11ecfd7c3 100644 --- a/apps/webapp/test/realtime/runChangeNotifier.test.ts +++ b/apps/webapp/test/realtime/runChangeNotifier.test.ts @@ -29,7 +29,9 @@ describe("RunChangeNotifier", () => { const notifier = new RunChangeNotifier({ redis: toRedisOptions(redisOptions) }); try { const received: ChangeRecord[] = []; - const unsubscribe = notifier.subscribeToEnv("env_1", (records) => received.push(...records)); + const unsubscribe = notifier.subscribeToEnv("env_1", (records) => + received.push(...records) + ); expect(notifier.activeSubscriptionCount).toBe(1); await sleep(SUBSCRIBE_SETTLE_MS); diff --git a/apps/webapp/test/realtime/shadowCompare.test.ts b/apps/webapp/test/realtime/shadowCompare.test.ts index 0d5f431f0b..7bbc985edc 100644 --- a/apps/webapp/test/realtime/shadowCompare.test.ts +++ b/apps/webapp/test/realtime/shadowCompare.test.ts @@ -120,7 +120,12 @@ describe("RealtimeShadowComparator serialization", () => { expect(out.serializationDiverged).toBe(1); expect(out.serializationMatched).toBe(0); expect(out.diffs).toEqual([ - { runId: "run_a", column: "payload", electric: '{"hello":"TAMPERED"}', native: '{"hello":"world"}' }, + { + runId: "run_a", + column: "payload", + electric: '{"hello":"TAMPERED"}', + native: '{"hello":"world"}', + }, ]); }); @@ -145,7 +150,9 @@ describe("RealtimeShadowComparator serialization", () => { it("records skew when the row advanced between emit and refetch", async () => { const row = sampleRow(); // Electric emitted an older version; the refetched row is newer. - const value = { ...serializeRunRow(sampleRow({ updatedAt: new Date("2026-06-07T10:00:00.000Z") })) }; + const value = { + ...serializeRunRow(sampleRow({ updatedAt: new Date("2026-06-07T10:00:00.000Z") })), + }; const body = JSON.stringify([insert(value), UP_TO_DATE]); const cmp = makeComparator({ run_a: row }); @@ -179,10 +186,10 @@ describe("RealtimeShadowComparator membership", () => { } it("matches when Electric's set equals the native resolver's set", async () => { - const cmp = makeComparator( - { a: sampleRow({ id: "a" }), b: sampleRow({ id: "b" }) }, - ["a", "b"] - ); + const cmp = makeComparator({ a: sampleRow({ id: "a" }), b: sampleRow({ id: "b" }) }, [ + "a", + "b", + ]); const out = await cmp.compare({ feed: "runs", electricBody: bodyFor(["a", "b"]), diff --git a/apps/webapp/test/replay-after-crash.test.ts b/apps/webapp/test/replay-after-crash.test.ts index fdd5274b5e..6133bda843 100644 --- a/apps/webapp/test/replay-after-crash.test.ts +++ b/apps/webapp/test/replay-after-crash.test.ts @@ -62,11 +62,7 @@ function textTurn(id: string, text: string): UIMessageChunk[] { * the schema now declares `data: z.unknown()` and consumers use it * without an extra `JSON.parse` step. */ -function stubApiClient(opts: { - projectRef: string; - envSlug: string; - sessionOutChunks: unknown[]; -}) { +function stubApiClient(opts: { projectRef: string; envSlug: string; sessionOutChunks: unknown[] }) { const records = opts.sessionOutChunks.map((chunk, i) => ({ data: chunk, id: `evt-${i + 1}`, @@ -220,7 +216,11 @@ describe("replay after crash (MinIO + SDK helpers)", () => { sessionOutChunks: [ ...textTurn("a-complete", "I finished step 1"), // Partial tool turn β€” no tool-input-end, no finish. - { type: "start", messageId: "a-orphan", messageMetadata: { role: "assistant" } } as UIMessageChunk, + { + type: "start", + messageId: "a-orphan", + messageMetadata: { role: "assistant" }, + } as UIMessageChunk, { type: "tool-input-start", id: "tc-cut", toolName: "search" } as UIMessageChunk, { type: "tool-input-delta", id: "tc-cut", delta: '{"q":"x"}' } as UIMessageChunk, ], @@ -233,9 +233,9 @@ describe("replay after crash (MinIO + SDK helpers)", () => { // Orphaned tool-call never surfaces in `input-streaming` state. const orphan = replayed.find((m) => m.id === "a-orphan"); if (orphan) { - const stillStreaming = (orphan.parts as Array<{ toolCallId?: string; state?: string }>).find( - (p) => p.toolCallId === "tc-cut" && p.state === "input-streaming" - ); + const stillStreaming = ( + orphan.parts as Array<{ toolCallId?: string; state?: string }> + ).find((p) => p.toolCallId === "tc-cut" && p.state === "input-streaming"); expect(stillStreaming).toBeUndefined(); } } @@ -282,9 +282,8 @@ describe("replay after crash (MinIO + SDK helpers)", () => { envSlug: "dev", sessionOutChunks: [], }); - const { __writeChatSnapshotProductionPathForTests: writeSnapshot } = await import( - "@trigger.dev/sdk/ai" - ); + const { __writeChatSnapshotProductionPathForTests: writeSnapshot } = + await import("@trigger.dev/sdk/ai"); await writeSnapshot(sessionId, snapshot); // Restubbing for the boot phase: replay tail carries the fresh diff --git a/apps/webapp/test/runsRepositoryCursor.test.ts b/apps/webapp/test/runsRepositoryCursor.test.ts index f7e02763ad..ec447245da 100644 --- a/apps/webapp/test/runsRepositoryCursor.test.ts +++ b/apps/webapp/test/runsRepositoryCursor.test.ts @@ -290,10 +290,7 @@ describe("RunsRepository cursor pagination", () => { }); const returned = page.runs.map((r) => r.id).sort(); - expect(returned).toEqual([ - "aaaaaaaaaaaaaaaaaaaaaaaa", - "bbbbbbbbbbbbbbbbbbbbbbbb", - ]); + expect(returned).toEqual(["aaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbb"]); } ); diff --git a/apps/webapp/test/sanitizeRowsOnParseError.test.ts b/apps/webapp/test/sanitizeRowsOnParseError.test.ts index 6e0de52aa6..e335317662 100644 --- a/apps/webapp/test/sanitizeRowsOnParseError.test.ts +++ b/apps/webapp/test/sanitizeRowsOnParseError.test.ts @@ -90,7 +90,12 @@ describe("sanitizeUnknownInPlace", () => { }); it("walks arrays recursively", () => { - const value = ["ok", `bad ${LOW_SURROGATE} value`, "also ok", { nested: `also bad ${HIGH_SURROGATE}` }]; + const value = [ + "ok", + `bad ${LOW_SURROGATE} value`, + "also ok", + { nested: `also bad ${HIGH_SURROGATE}` }, + ]; const result = sanitizeUnknownInPlace(value); expect(result.fixed).toBe(2); expect(value[1]).toBe(INVALID_UTF16_SENTINEL); @@ -196,9 +201,9 @@ describe("sanitizeUnknownInPlace", () => { }; const result = sanitizeUnknownInPlace(row); expect(result.fixed).toBe(1); - expect( - (row.output.data.profiles[1].spec_format![0].platform_variables[0] as any).value - ).toBe("117039831458782870000"); + expect((row.output.data.profiles[1].spec_format![0].platform_variables[0] as any).value).toBe( + "117039831458782870000" + ); // Untouched neighbours expect(row.output.data.profiles[0].module).toBe("linktree"); expect(row.output.data.profiles[1].spec_format![0].platform_variables[0].type).toBe("int"); diff --git a/apps/webapp/test/sessionDuration.test.ts b/apps/webapp/test/sessionDuration.test.ts index e10b4b5f21..a066865a4b 100644 --- a/apps/webapp/test/sessionDuration.test.ts +++ b/apps/webapp/test/sessionDuration.test.ts @@ -138,19 +138,16 @@ describe("getOrganizationSessionCap", () => { }); describe("getEffectiveSessionDuration", () => { - containerTest( - "returns the user setting when no org cap is set", - async ({ prisma }) => { - const user = await createUser(prisma, "effective-no-cap@test.com", oneDay); - await createOrgWithMember(prisma, "effective-no-cap-org", user.id, null); + containerTest("returns the user setting when no org cap is set", async ({ prisma }) => { + const user = await createUser(prisma, "effective-no-cap@test.com", oneDay); + await createOrgWithMember(prisma, "effective-no-cap-org", user.id, null); - const result = await getEffectiveSessionDuration(user.id, prisma); - expect(result.userSettingSeconds).toBe(oneDay); - expect(result.orgCapSeconds).toBeNull(); - expect(result.cappingOrgId).toBeNull(); - expect(result.durationSeconds).toBe(oneDay); - } - ); + const result = await getEffectiveSessionDuration(user.id, prisma); + expect(result.userSettingSeconds).toBe(oneDay); + expect(result.orgCapSeconds).toBeNull(); + expect(result.cappingOrgId).toBeNull(); + expect(result.durationSeconds).toBe(oneDay); + }); containerTest("caps the user setting at the most restrictive org cap", async ({ prisma }) => { const user = await createUser(prisma, "effective-capped@test.com", oneYear); diff --git a/apps/webapp/test/shouldRevalidateRunsList.test.ts b/apps/webapp/test/shouldRevalidateRunsList.test.ts index 2274ddddd2..5a9ace3089 100644 --- a/apps/webapp/test/shouldRevalidateRunsList.test.ts +++ b/apps/webapp/test/shouldRevalidateRunsList.test.ts @@ -30,7 +30,10 @@ describe("shouldRevalidateRunsList", () => { it("returns false when only bulk inspector UI params change", () => { expect( shouldRevalidateRunsList( - args("?tasks=hello", `?tasks=hello&bulkInspector=${RUNS_BULK_INSPECTOR_OPEN_VALUE}&action=replay&mode=selected`) + args( + "?tasks=hello", + `?tasks=hello&bulkInspector=${RUNS_BULK_INSPECTOR_OPEN_VALUE}&action=replay&mode=selected` + ) ) ).toBe(false); }); @@ -44,14 +47,14 @@ describe("shouldRevalidateRunsList", () => { }); it("returns false when list-data params are reordered", () => { - expect( - shouldRevalidateRunsList(args("?tasks=a&runtime=b", "?runtime=b&tasks=a")) - ).toBe(false); + expect(shouldRevalidateRunsList(args("?tasks=a&runtime=b", "?runtime=b&tasks=a"))).toBe(false); }); it("returns default when list filters and bulk inspector UI params change together", () => { expect( - shouldRevalidateRunsList(args("?tasks=a", `?tasks=b&bulkInspector=${RUNS_BULK_INSPECTOR_OPEN_VALUE}`)) + shouldRevalidateRunsList( + args("?tasks=a", `?tasks=b&bulkInspector=${RUNS_BULK_INSPECTOR_OPEN_VALUE}`) + ) ).toBe(true); }); @@ -61,9 +64,7 @@ describe("shouldRevalidateRunsList", () => { it("returns default when pagination params change", () => { expect( - shouldRevalidateRunsList( - args("?tasks=hello", "?tasks=hello&cursor=abc&direction=forward") - ) + shouldRevalidateRunsList(args("?tasks=hello", "?tasks=hello&cursor=abc&direction=forward")) ).toBe(true); }); @@ -81,9 +82,7 @@ describe("shouldRevalidateRunsList", () => { }); it("respects defaultShouldRevalidate when false", () => { - expect( - shouldRevalidateRunsList(args("?tasks=hello", "?tasks=world", false)) - ).toBe(false); + expect(shouldRevalidateRunsList(args("?tasks=hello", "?tasks=world", false))).toBe(false); }); }); @@ -98,10 +97,7 @@ function makeLocation(search: string): Location { }; } -function navigation( - state: Navigation["state"], - nextSearch?: string -): Navigation { +function navigation(state: Navigation["state"], nextSearch?: string): Navigation { return { state, location: nextSearch ? makeLocation(nextSearch) : undefined, @@ -121,7 +117,10 @@ describe("isRunsListLoading", () => { it("returns false when only bulk inspector UI params change", () => { expect( isRunsListLoading( - navigation("loading", `?tasks=hello&bulkInspector=${RUNS_BULK_INSPECTOR_OPEN_VALUE}&action=replay`), + navigation( + "loading", + `?tasks=hello&bulkInspector=${RUNS_BULK_INSPECTOR_OPEN_VALUE}&action=replay` + ), "?tasks=hello" ) ).toBe(false); @@ -135,14 +134,15 @@ describe("isRunsListLoading", () => { it("returns true when list filters and bulk inspector UI params change together", () => { expect( - isRunsListLoading(navigation("loading", `?tasks=b&bulkInspector=${RUNS_BULK_INSPECTOR_OPEN_VALUE}`), "?tasks=a") + isRunsListLoading( + navigation("loading", `?tasks=b&bulkInspector=${RUNS_BULK_INSPECTOR_OPEN_VALUE}`), + "?tasks=a" + ) ).toBe(true); }); it("returns true when list filters change", () => { - expect( - isRunsListLoading(navigation("loading", "?tasks=world"), "?tasks=hello") - ).toBe(true); + expect(isRunsListLoading(navigation("loading", "?tasks=world"), "?tasks=hello")).toBe(true); }); it("returns true when pagination params change", () => { diff --git a/apps/webapp/test/slackErrorAlerts.test.ts b/apps/webapp/test/slackErrorAlerts.test.ts index b86856adc4..3a326b21ac 100644 --- a/apps/webapp/test/slackErrorAlerts.test.ts +++ b/apps/webapp/test/slackErrorAlerts.test.ts @@ -67,8 +67,7 @@ function createMockErrorPayload( // Skip tests if Slack credentials not configured const hasSlackCredentials = - !!process.env.TEST_SLACK_CHANNEL_ID && - !!process.env.TEST_SLACK_BOT_TOKEN; + !!process.env.TEST_SLACK_CHANNEL_ID && !!process.env.TEST_SLACK_BOT_TOKEN; describe.skipIf(!hasSlackCredentials)("Slack Error Alert Visual Tests", () => { beforeAll(async () => { @@ -76,9 +75,7 @@ describe.skipIf(!hasSlackCredentials)("Slack Error Alert Visual Tests", () => { prisma = dbModule.prisma; const secretModule = await import("../app/services/secrets/secretStore.server.js"); getSecretStore = secretModule.getSecretStore; - const alertModule = await import( - "../app/v3/services/alerts/deliverErrorGroupAlert.server.js" - ); + const alertModule = await import("../app/v3/services/alerts/deliverErrorGroupAlert.server.js"); DeliverErrorGroupAlertService = alertModule.DeliverErrorGroupAlertService; const organization = await prisma.organization.create({ diff --git a/apps/webapp/test/timelineSpanEvents.test.ts b/apps/webapp/test/timelineSpanEvents.test.ts index 11b62cf5af..4118be0755 100644 --- a/apps/webapp/test/timelineSpanEvents.test.ts +++ b/apps/webapp/test/timelineSpanEvents.test.ts @@ -27,8 +27,7 @@ describe("createTimelineSpanEventsFromSpanEvents", () => { file: "src/trigger/chat.ts", event: "import", duration: 67, - entryPoint: - "/project/.trigger/tmp/build-AL7zTl/src/trigger/chat.mjs", + entryPoint: "/project/.trigger/tmp/build-AL7zTl/src/trigger/chat.mjs", }, }, ]; @@ -52,8 +51,7 @@ describe("createTimelineSpanEventsFromSpanEvents", () => { file: "src/trigger/chat.ts", event: "import", duration: 67, - entryPoint: - "/project/.trigger/tmp/build-AL7zTl/src/trigger/chat.mjs", + entryPoint: "/project/.trigger/tmp/build-AL7zTl/src/trigger/chat.mjs", }, }, ]; diff --git a/apps/webapp/test/traceExport.test.ts b/apps/webapp/test/traceExport.test.ts index a05187795d..35816b2027 100644 --- a/apps/webapp/test/traceExport.test.ts +++ b/apps/webapp/test/traceExport.test.ts @@ -45,7 +45,8 @@ function sampleEvents(): StreamedTraceEvent[] { level: "ERROR", message: "task failed", isError: true, - propertiesText: '{"error":{"message":"boom: it failed","name":"Error","stackTrace":"Error: boom\\n at fn"}}', + propertiesText: + '{"error":{"message":"boom: it failed","name":"Error","stackTrace":"Error: boom\\n at fn"}}', }, { spanId: "quiet1", @@ -75,7 +76,9 @@ async function drain(gen: AsyncIterable): Promise { } function render(formatName: string, items = sampleEvents(), opts = {}): Promise { - return drain(streamTraceExport(toAsyncIterable(items), getTraceExportFormat(formatName), CTX, opts)); + return drain( + streamTraceExport(toAsyncIterable(items), getTraceExportFormat(formatName), CTX, opts) + ); } describe("getTraceExportFormat", () => { @@ -142,7 +145,9 @@ describe("markdown format", () => { expect(text).toContain("task: agent-workflow"); expect(text).toContain("url: https://app.example.com/orgs/o/projects/p/env/dev/runs/run_x"); expect(text).toContain("# Trace for run_x"); - expect(text).toContain("[View in dashboard](https://app.example.com/orgs/o/projects/p/env/dev/runs/run_x)"); + expect(text).toContain( + "[View in dashboard](https://app.example.com/orgs/o/projects/p/env/dev/runs/run_x)" + ); expect(text).toContain("| time | level | event | duration | span ← parent | properties |"); expect(text).not.toContain("```json"); expect(text).toContain("`log1 ← root1`"); diff --git a/apps/webapp/test/utils/testReplicationClickhouseFactory.ts b/apps/webapp/test/utils/testReplicationClickhouseFactory.ts index 2422a461d6..5108ae7910 100644 --- a/apps/webapp/test/utils/testReplicationClickhouseFactory.ts +++ b/apps/webapp/test/utils/testReplicationClickhouseFactory.ts @@ -1,8 +1,5 @@ import type { ClickHouse } from "@internal/clickhouse"; -import { - ClickhouseFactory, - type ClientType, -} from "~/services/clickhouse/clickhouseFactory.server"; +import { ClickhouseFactory, type ClientType } from "~/services/clickhouse/clickhouseFactory.server"; import type { OrganizationDataStoresRegistry } from "~/services/dataStores/organizationDataStoresRegistry.server"; const testReplicationRegistryStub = { diff --git a/apps/webapp/test/workerRegions.test.ts b/apps/webapp/test/workerRegions.test.ts index 7befdfee84..9645e8a9ff 100644 --- a/apps/webapp/test/workerRegions.test.ts +++ b/apps/webapp/test/workerRegions.test.ts @@ -1,10 +1,32 @@ import { describe, it, expect } from "vitest"; -import { regionForQueue, backingForQueue, type WorkerGroupRegionRow } from "~/v3/workerRegions.server"; +import { + regionForQueue, + backingForQueue, + type WorkerGroupRegionRow, +} from "~/v3/workerRegions.server"; const groups: WorkerGroupRegionRow[] = [ - { masterQueue: "us-east-1", region: "us-east-1", workloadType: "CONTAINER", hidden: false, enableFastPath: false }, - { masterQueue: "us-east-1-next", region: "us-east-1", workloadType: "MICROVM", hidden: false, enableFastPath: true }, - { masterQueue: "eu-central-1", region: "eu-central-1", workloadType: "CONTAINER", hidden: false, enableFastPath: false }, + { + masterQueue: "us-east-1", + region: "us-east-1", + workloadType: "CONTAINER", + hidden: false, + enableFastPath: false, + }, + { + masterQueue: "us-east-1-next", + region: "us-east-1", + workloadType: "MICROVM", + hidden: false, + enableFastPath: true, + }, + { + masterQueue: "eu-central-1", + region: "eu-central-1", + workloadType: "CONTAINER", + hidden: false, + enableFastPath: false, + }, ]; describe("regionForQueue", () => { @@ -18,24 +40,59 @@ describe("regionForQueue", () => { expect(regionForQueue("mystery", groups)).toBe("mystery"); }); it("passes through when a group has no region set", () => { - expect(regionForQueue("x", [{ masterQueue: "x", region: null, workloadType: "CONTAINER", hidden: false, enableFastPath: false }])).toBe("x"); + expect( + regionForQueue("x", [ + { + masterQueue: "x", + region: null, + workloadType: "CONTAINER", + hidden: false, + enableFastPath: false, + }, + ]) + ).toBe("x"); }); }); describe("backingForQueue", () => { it("finds the MICROVM backing for a region with one", () => { - expect(backingForQueue("us-east-1", groups)).toEqual({ workerQueue: "us-east-1-next", enableFastPath: true }); + expect(backingForQueue("us-east-1", groups)).toEqual({ + workerQueue: "us-east-1-next", + enableFastPath: true, + }); }); it("returns undefined for a region with no compute backing (EU)", () => { expect(backingForQueue("eu-central-1", groups)).toBeUndefined(); }); it("returns undefined when the queue's group has no region", () => { - expect(backingForQueue("x", [{ masterQueue: "x", region: null, workloadType: "CONTAINER", hidden: false, enableFastPath: false }])).toBeUndefined(); + expect( + backingForQueue("x", [ + { + masterQueue: "x", + region: null, + workloadType: "CONTAINER", + hidden: false, + enableFastPath: false, + }, + ]) + ).toBeUndefined(); }); it("ignores hidden MICROVM groups", () => { const g: WorkerGroupRegionRow[] = [ - { masterQueue: "us-east-1", region: "us-east-1", workloadType: "CONTAINER", hidden: false, enableFastPath: false }, - { masterQueue: "us-east-1-next", region: "us-east-1", workloadType: "MICROVM", hidden: true, enableFastPath: true }, + { + masterQueue: "us-east-1", + region: "us-east-1", + workloadType: "CONTAINER", + hidden: false, + enableFastPath: false, + }, + { + masterQueue: "us-east-1-next", + region: "us-east-1", + workloadType: "MICROVM", + hidden: true, + enableFastPath: true, + }, ]; expect(backingForQueue("us-east-1", g)).toBeUndefined(); }); diff --git a/depot.json b/depot.json index 60fd90c6ec..b1b6660dd5 100644 --- a/depot.json +++ b/depot.json @@ -1,3 +1,3 @@ { "id": "g2k5ln95n6" -} \ No newline at end of file +} diff --git a/docker/config/grafana/provisioning/dashboards/batch-queue.json b/docker/config/grafana/provisioning/dashboards/batch-queue.json index 1c154b43fe..7d27c67d69 100644 --- a/docker/config/grafana/provisioning/dashboards/batch-queue.json +++ b/docker/config/grafana/provisioning/dashboards/batch-queue.json @@ -186,7 +186,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, "id": 6, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -238,7 +243,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, "id": 7, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -298,7 +308,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "id": 9, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -355,7 +370,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "id": 10, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -420,7 +440,12 @@ "gridPos": { "h": 8, "w": 8, "x": 0, "y": 25 }, "id": 12, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -467,7 +492,12 @@ "gridPos": { "h": 8, "w": 8, "x": 8, "y": 25 }, "id": 13, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -561,7 +591,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 34 }, "id": 16, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -618,7 +653,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 34 }, "id": 17, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -683,7 +723,12 @@ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 43 }, "id": 19, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ diff --git a/docker/config/grafana/provisioning/dashboards/dashboards.yml b/docker/config/grafana/provisioning/dashboards/dashboards.yml index bdb2d1b713..9763da209d 100644 --- a/docker/config/grafana/provisioning/dashboards/dashboards.yml +++ b/docker/config/grafana/provisioning/dashboards/dashboards.yml @@ -14,4 +14,3 @@ providers: allowUiUpdates: true options: path: /etc/grafana/provisioning/dashboards - diff --git a/docker/config/grafana/provisioning/dashboards/nodejs-runtime.json b/docker/config/grafana/provisioning/dashboards/nodejs-runtime.json index 9f190b4977..1a432bcaaf 100644 --- a/docker/config/grafana/provisioning/dashboards/nodejs-runtime.json +++ b/docker/config/grafana/provisioning/dashboards/nodejs-runtime.json @@ -142,9 +142,7 @@ "mappings": [], "thresholds": { "mode": "absolute", - "steps": [ - { "color": "green", "value": null } - ] + "steps": [{ "color": "green", "value": null }] }, "unit": "short" }, @@ -207,7 +205,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, "id": 6, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -254,7 +257,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, "id": 7, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -324,7 +332,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "id": 9, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -371,7 +384,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "id": 10, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -418,7 +436,12 @@ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 24 }, "id": 11, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -443,4 +466,3 @@ "uid": "nodejs-runtime", "version": 1 } - diff --git a/docker/config/grafana/provisioning/dashboards/realtime-native.json b/docker/config/grafana/provisioning/dashboards/realtime-native.json index 832f2c8e32..5bc21240c2 100644 --- a/docker/config/grafana/provisioning/dashboards/realtime-native.json +++ b/docker/config/grafana/provisioning/dashboards/realtime-native.json @@ -1,10 +1,7 @@ { "title": "Realtime Native Backend", "uid": "realtime-native", - "tags": [ - "trigger.dev", - "realtime" - ], + "tags": ["trigger.dev", "realtime"], "timezone": "browser", "schemaVersion": 39, "refresh": "10s", @@ -174,9 +171,7 @@ "colorMode": "background", "graphMode": "area", "reduceOptions": { - "calcs": [ - "lastNotNull" - ] + "calcs": ["lastNotNull"] } }, "description": "A backstop that finds missed changes means the notify/replay path is leaking. Alert on sustained non-zero." @@ -500,4 +495,4 @@ "description": "hit/coalesced vs miss = the single-flight cache collapsing same-filter herds. Gate in use near the limit = reconnect stampedes queueing." } ] -} \ No newline at end of file +} diff --git a/docker/config/grafana/provisioning/dashboards/runs-replication.json b/docker/config/grafana/provisioning/dashboards/runs-replication.json index 680f260daa..2bdf91294c 100644 --- a/docker/config/grafana/provisioning/dashboards/runs-replication.json +++ b/docker/config/grafana/provisioning/dashboards/runs-replication.json @@ -186,7 +186,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, "id": 6, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -233,7 +238,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, "id": 7, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -293,7 +303,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "id": 9, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -350,7 +365,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "id": 10, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -415,7 +435,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 25 }, "id": 13, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -519,7 +544,12 @@ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 34 }, "id": 16, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ diff --git a/docker/config/grafana/provisioning/datasources/datasources.yml b/docker/config/grafana/provisioning/datasources/datasources.yml index 51194dbc06..52f2d64069 100644 --- a/docker/config/grafana/provisioning/datasources/datasources.yml +++ b/docker/config/grafana/provisioning/datasources/datasources.yml @@ -15,4 +15,3 @@ datasources: httpMethod: POST manageAlerts: true prometheusType: Prometheus - diff --git a/docker/config/prometheus.yml b/docker/config/prometheus.yml index b4ad879ffe..19e681de94 100644 --- a/docker/config/prometheus.yml +++ b/docker/config/prometheus.yml @@ -30,4 +30,3 @@ scrape_configs: - job_name: "prometheus" static_configs: - targets: ["localhost:9090"] - diff --git a/docker/config/toxiproxy.json b/docker/config/toxiproxy.json index 3462471672..435bf0c55b 100644 --- a/docker/config/toxiproxy.json +++ b/docker/config/toxiproxy.json @@ -5,4 +5,4 @@ "upstream": "host.docker.internal:3030", "enabled": true } -] \ No newline at end of file +] diff --git a/docs/docs.json b/docs/docs.json index b74563300d..f373503049 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -10,11 +10,7 @@ }, "favicon": "/images/favicon.png", "contextual": { - "options": [ - "copy", - "view", - "claude" - ] + "options": ["copy", "view", "claude"] }, "navigation": { "dropdowns": [ @@ -40,11 +36,7 @@ "pages": [ { "group": "Tasks", - "pages": [ - "tasks/overview", - "tasks/schemaTask", - "tasks/scheduled" - ] + "pages": ["tasks/overview", "tasks/schemaTask", "tasks/scheduled"] }, "triggering", "runs", @@ -57,10 +49,7 @@ "building-with-ai", { "group": "MCP Server", - "pages": [ - "mcp-introduction", - "mcp-tools" - ] + "pages": ["mcp-introduction", "mcp-tools"] }, "skills", "mcp-agent-rules" @@ -74,12 +63,7 @@ "errors-retrying", { "group": "Wait", - "pages": [ - "wait", - "wait-for", - "wait-until", - "wait-for-token" - ] + "pages": ["wait", "wait-for", "wait-until", "wait-for-token"] }, "queue-concurrency", "versioning", @@ -203,10 +187,7 @@ "deployment/atomic-deployment", { "group": "Deployment integrations", - "pages": [ - "github-integration", - "vercel-integration" - ] + "pages": ["github-integration", "vercel-integration"] } ] }, @@ -270,19 +251,11 @@ }, { "group": "Observability", - "pages": [ - "observability/query", - "observability/dashboards" - ] + "pages": ["observability/query", "observability/dashboards"] }, { "group": "Using the Dashboard", - "pages": [ - "run-tests", - "troubleshooting-alerts", - "replaying", - "bulk-actions" - ] + "pages": ["run-tests", "troubleshooting-alerts", "replaying", "bulk-actions"] }, { "group": "Troubleshooting", @@ -305,30 +278,18 @@ "self-hosting/kubernetes", { "group": "Environment variables", - "pages": [ - "self-hosting/env/webapp", - "self-hosting/env/supervisor" - ] + "pages": ["self-hosting/env/webapp", "self-hosting/env/supervisor"] }, "open-source-self-hosting" ] }, { "group": "Open source", - "pages": [ - "open-source-contributing", - "github-repo", - "changelog", - "roadmap" - ] + "pages": ["open-source-contributing", "github-repo", "changelog", "roadmap"] }, { "group": "Help", - "pages": [ - "community", - "help-slack", - "help-email" - ] + "pages": ["community", "help-slack", "help-email"] } ] }, @@ -471,9 +432,7 @@ "groups": [ { "group": "Introduction", - "pages": [ - "guides/introduction" - ] + "pages": ["guides/introduction"] }, { "group": "Frameworks", @@ -539,10 +498,7 @@ }, { "group": "Migration guides", - "pages": [ - "migration-mergent", - "migration-n8n" - ] + "pages": ["migration-mergent", "migration-n8n"] }, { "group": "Use cases", @@ -633,10 +589,7 @@ "href": "https://trigger.dev" }, "api": { - "openapi": [ - "openapi.yml", - "v3-openapi.yaml" - ], + "openapi": ["openapi.yml", "v3-openapi.yaml"], "playground": { "display": "simple" } diff --git a/docs/style.css b/docs/style.css index 94aea582db..91cf9cd270 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,31 +1,31 @@ -button~.absolute.peer-hover\:opacity-100 { - color: #000 +button ~ .absolute.peer-hover\:opacity-100 { + color: #000; } :root { - /* Code block colors - Trigger.dark theme */ - --mint-color-background: #121317; - --mint-color-text: #D4D4D4; - --mint-token-constant: #9B99FF; - --mint-token-string: #AFEC73; - --mint-token-comment: #5F6570; - --mint-token-keyword: #E888F8; - --mint-token-parameter: #CCCBFF; - --mint-token-function: #D9F07C; - --mint-token-string-expression: #AFEC73; - --mint-token-punctuation: #878C99; - --mint-token-link: #826DFF; + /* Code block colors - Trigger.dark theme */ + --mint-color-background: #121317; + --mint-color-text: #d4d4d4; + --mint-token-constant: #9b99ff; + --mint-token-string: #afec73; + --mint-token-comment: #5f6570; + --mint-token-keyword: #e888f8; + --mint-token-parameter: #cccbff; + --mint-token-function: #d9f07c; + --mint-token-string-expression: #afec73; + --mint-token-punctuation: #878c99; + --mint-token-link: #826dff; - /* Shiki css-variables fallbacks */ - --shiki-foreground: #D4D4D4; - --shiki-background: #121317; - --shiki-token-constant: #9B99FF; - --shiki-token-string: #AFEC73; - --shiki-token-comment: #5F6570; - --shiki-token-keyword: #E888F8; - --shiki-token-parameter: #CCCBFF; - --shiki-token-function: #D9F07C; - --shiki-token-string-expression: #AFEC73; - --shiki-token-punctuation: #878C99; - --shiki-token-link: #826DFF; + /* Shiki css-variables fallbacks */ + --shiki-foreground: #d4d4d4; + --shiki-background: #121317; + --shiki-token-constant: #9b99ff; + --shiki-token-string: #afec73; + --shiki-token-comment: #5f6570; + --shiki-token-keyword: #e888f8; + --shiki-token-parameter: #cccbff; + --shiki-token-function: #d9f07c; + --shiki-token-string-expression: #afec73; + --shiki-token-punctuation: #878c99; + --shiki-token-link: #826dff; } diff --git a/hosting/docker/webapp/docker-compose.yml b/hosting/docker/webapp/docker-compose.yml index d246babf95..7b2de751b7 100644 --- a/hosting/docker/webapp/docker-compose.yml +++ b/hosting/docker/webapp/docker-compose.yml @@ -238,4 +238,4 @@ networks: supervisor: name: supervisor webapp: - name: webapp \ No newline at end of file + name: webapp diff --git a/hosting/docker/worker/docker-compose.yml b/hosting/docker/worker/docker-compose.yml index 6e9f4db272..a69605949e 100644 --- a/hosting/docker/worker/docker-compose.yml +++ b/hosting/docker/worker/docker-compose.yml @@ -45,7 +45,13 @@ services: DOCKER_REGISTRY_PASSWORD: ${DOCKER_REGISTRY_PASSWORD:-} DOCKER_AUTOREMOVE_EXITED_CONTAINERS: 0 healthcheck: - test: ["CMD", "node", "-e", "http.get('http://localhost:8020/health', res => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + test: + [ + "CMD", + "node", + "-e", + "http.get('http://localhost:8020/health', res => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))", + ] interval: 30s timeout: 10s retries: 5 diff --git a/internal-packages/cache/package.json b/internal-packages/cache/package.json index 02ba86e1fd..8d1bec36c7 100644 --- a/internal-packages/cache/package.json +++ b/internal-packages/cache/package.json @@ -18,4 +18,4 @@ "test": "vitest", "test:watch": "vitest" } -} \ No newline at end of file +} diff --git a/internal-packages/cache/src/stores/lruMemory.ts b/internal-packages/cache/src/stores/lruMemory.ts index 63b1d5cd4b..5cd736dec5 100644 --- a/internal-packages/cache/src/stores/lruMemory.ts +++ b/internal-packages/cache/src/stores/lruMemory.ts @@ -28,9 +28,10 @@ export type LRUMemoryStoreConfig = { * but will be evicted by LRU when the cache is full. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export class LRUMemoryStore - implements Store -{ +export class LRUMemoryStore implements Store< + TNamespace, + TValue +> { readonly name: string; private readonly cache: LRUCache>; @@ -110,10 +111,7 @@ export class LRUMemoryStore } } - async remove( - namespace: TNamespace, - keys: string | string[] - ): Promise> { + async remove(namespace: TNamespace, keys: string | string[]): Promise> { try { const keyArray = Array.isArray(keys) ? keys : [keys]; diff --git a/internal-packages/cache/src/stores/redis.ts b/internal-packages/cache/src/stores/redis.ts index 4f40133a4f..456b5813cb 100644 --- a/internal-packages/cache/src/stores/redis.ts +++ b/internal-packages/cache/src/stores/redis.ts @@ -9,9 +9,10 @@ export type RedisCacheStoreConfig = { useModernCacheKeyBuilder?: boolean; }; -export class RedisCacheStore - implements Store -{ +export class RedisCacheStore implements Store< + TNamespace, + TValue +> { public readonly name = "redis"; private readonly redis: Redis; diff --git a/internal-packages/clickhouse/src/client/client.ts b/internal-packages/clickhouse/src/client/client.ts index d281b60482..29323d1418 100644 --- a/internal-packages/clickhouse/src/client/client.ts +++ b/internal-packages/clickhouse/src/client/client.ts @@ -518,7 +518,10 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter { }; } - public queryFastStream, TParams extends Record>(req: { + public queryFastStream< + TOut extends Record, + TParams extends Record, + >(req: { name: string; query: string; columns: Array; diff --git a/internal-packages/clickhouse/src/client/noop.ts b/internal-packages/clickhouse/src/client/noop.ts index 9f212c3d16..0e7f2416c1 100644 --- a/internal-packages/clickhouse/src/client/noop.ts +++ b/internal-packages/clickhouse/src/client/noop.ts @@ -95,7 +95,10 @@ export class NoopClient implements ClickhouseReader, ClickhouseWriter { }; } - public queryFastStream, TParams extends Record>(req: { + public queryFastStream< + TOut extends Record, + TParams extends Record, + >(req: { name: string; query: string; columns: string[]; diff --git a/internal-packages/clickhouse/src/client/tsql.ts b/internal-packages/clickhouse/src/client/tsql.ts index 0a8367e717..f3cf9067be 100644 --- a/internal-packages/clickhouse/src/client/tsql.ts +++ b/internal-packages/clickhouse/src/client/tsql.ts @@ -15,7 +15,7 @@ import { type QuerySettings, type FieldMappings, type TimeRange, - type WhereClauseCondition + type WhereClauseCondition, } from "@internal/tsql"; import type { ClickhouseReader, QueryStats } from "./types.js"; import { QueryError } from "./errors.js"; @@ -180,9 +180,10 @@ export async function executeTSQL( try { // 1. Compile the TSQL query to ClickHouse SQL // Pass maxRows + 1 to fetch one extra row for overflow detection - const compiledSettings = maxRows !== undefined - ? { ...options.querySettings, maxRows: maxRows + 1 } - : options.querySettings; + const compiledSettings = + maxRows !== undefined + ? { ...options.querySettings, maxRows: maxRows + 1 } + : options.querySettings; const { sql, params, columns, hiddenColumns } = compileTSQL(options.query, { tableSchema: options.tableSchema, diff --git a/internal-packages/clickhouse/src/sessions.ts b/internal-packages/clickhouse/src/sessions.ts index 995e98447e..bebb8b1549 100644 --- a/internal-packages/clickhouse/src/sessions.ts +++ b/internal-packages/clickhouse/src/sessions.ts @@ -157,10 +157,7 @@ export function getSessionsQueryBuilder(ch: ClickhouseReader, settings?: ClickHo }); } -export function getSessionsCountQueryBuilder( - ch: ClickhouseReader, - settings?: ClickHouseSettings -) { +export function getSessionsCountQueryBuilder(ch: ClickhouseReader, settings?: ClickHouseSettings) { return ch.queryBuilder({ name: "getSessionsCount", baseQuery: "SELECT count() as count FROM trigger_dev.sessions_v1 FINAL", @@ -175,10 +172,7 @@ export const SessionTagsQueryResult = z.object({ export type SessionTagsQueryResult = z.infer; -export function getSessionTagsQueryBuilder( - ch: ClickhouseReader, - settings?: ClickHouseSettings -) { +export function getSessionTagsQueryBuilder(ch: ClickhouseReader, settings?: ClickHouseSettings) { return ch.queryBuilder({ name: "getSessionTags", baseQuery: "SELECT DISTINCT arrayJoin(tags) as tag FROM trigger_dev.sessions_v1", diff --git a/internal-packages/clickhouse/src/taskEvents.ts b/internal-packages/clickhouse/src/taskEvents.ts index e8d4c2bd95..4c17d5068d 100644 --- a/internal-packages/clickhouse/src/taskEvents.ts +++ b/internal-packages/clickhouse/src/taskEvents.ts @@ -213,10 +213,7 @@ export function insertTaskEventsV2(ch: ClickhouseWriter, settings?: ClickHouseSe }); } -export function getTraceSummaryQueryBuilderV2( - ch: ClickhouseReader, - settings?: ClickHouseSettings -) { +export function getTraceSummaryQueryBuilderV2(ch: ClickhouseReader, settings?: ClickHouseSettings) { return ch.queryBuilderFast({ name: "getTraceEventsV2", table: "trigger_dev.task_events_v2", @@ -258,10 +255,7 @@ export function getTraceDetailedSummaryQueryBuilderV2( }); } -export function getSpanDetailsQueryBuilderV2( - ch: ClickhouseReader, - settings?: ClickHouseSettings -) { +export function getSpanDetailsQueryBuilderV2(ch: ClickhouseReader, settings?: ClickHouseSettings) { return ch.queryBuilder({ name: "getSpanDetailsV2", baseQuery: @@ -283,7 +277,6 @@ export function getTraceEventsForExportQueryBuilderV2( }); } - // ============================================================================ // Search Table Query Builders (for logs page, using task_events_search_v1) // ============================================================================ @@ -350,7 +343,7 @@ export const LogDetailV2Result = z.object({ kind: z.string(), status: z.string(), duration: z.number().or(z.string()), - attributes_text: z.string() + attributes_text: z.string(), }); export type LogDetailV2Result = z.output; @@ -376,4 +369,4 @@ export function getLogDetailQueryBuilderV2(ch: ClickhouseReader) { "attributes_text", ], }); -} \ No newline at end of file +} diff --git a/internal-packages/clickhouse/src/taskRuns.test.ts b/internal-packages/clickhouse/src/taskRuns.test.ts index fb64d458b1..0d4ec995c2 100644 --- a/internal-packages/clickhouse/src/taskRuns.test.ts +++ b/internal-packages/clickhouse/src/taskRuns.test.ts @@ -556,9 +556,7 @@ describe("Task Runs V2", () => { null, ]; - const childA_v2: TaskRunInsertArray = [ - ...childA_v1, - ]; + const childA_v2: TaskRunInsertArray = [...childA_v1]; childA_v2[TASK_RUN_INDEX.status] = "COMPLETED_SUCCESSFULLY"; childA_v2[TASK_RUN_INDEX._version] = "2"; @@ -676,24 +674,18 @@ describe("Task Runs V2", () => { null, ]; - const childDeleted_v2: TaskRunInsertArray = [ - ...childDeleted_v1, - ]; + const childDeleted_v2: TaskRunInsertArray = [...childDeleted_v1]; childDeleted_v2[TASK_RUN_INDEX._version] = "2"; childDeleted_v2[TASK_RUN_INDEX._is_deleted] = 1; - const childWrongRoot: TaskRunInsertArray = [ - ...childB, - ]; + const childWrongRoot: TaskRunInsertArray = [...childB]; childWrongRoot[TASK_RUN_INDEX.run_id] = "child_wrong_root"; childWrongRoot[TASK_RUN_INDEX.friendly_id] = "run_child_wrong_root"; childWrongRoot[TASK_RUN_INDEX.root_run_id] = "other_root"; childWrongRoot[TASK_RUN_INDEX.parent_run_id] = "other_root"; childWrongRoot[TASK_RUN_INDEX.span_id] = "span_child_wrong_root"; - const childOld: TaskRunInsertArray = [ - ...childB, - ]; + const childOld: TaskRunInsertArray = [...childB]; childOld[TASK_RUN_INDEX.run_id] = "child_old"; childOld[TASK_RUN_INDEX.created_at] = oldCreatedAt; childOld[TASK_RUN_INDEX.updated_at] = oldCreatedAt; diff --git a/internal-packages/clickhouse/src/tsql.test.ts b/internal-packages/clickhouse/src/tsql.test.ts index 5c1d6ed802..cb30d5e85c 100644 --- a/internal-packages/clickhouse/src/tsql.test.ts +++ b/internal-packages/clickhouse/src/tsql.test.ts @@ -1563,52 +1563,49 @@ describe("Field Mapping Tests", () => { } ); - clickhouseTest( - "should handle field mapping with IN clause", - async ({ clickhouseContainer }) => { - const client = new ClickhouseClient({ - name: "test", - url: clickhouseContainer.getConnectionUrl(), - }); + clickhouseTest("should handle field mapping with IN clause", async ({ clickhouseContainer }) => { + const client = new ClickhouseClient({ + name: "test", + url: clickhouseContainer.getConnectionUrl(), + }); - const insert = insertTaskRuns(client, { async_insert: 0 }); + const insert = insertTaskRuns(client, { async_insert: 0 }); - // Insert test runs with different projects - await insert([ - createTaskRun({ - run_id: "run_fm_in1", - project_id: "proj_tenant1", - status: "COMPLETED_SUCCESSFULLY", - }), - createTaskRun({ - run_id: "run_fm_in2", - project_id: "proj_other", - organization_id: "org_tenant1", - status: "PENDING", - }), - ]); + // Insert test runs with different projects + await insert([ + createTaskRun({ + run_id: "run_fm_in1", + project_id: "proj_tenant1", + status: "COMPLETED_SUCCESSFULLY", + }), + createTaskRun({ + run_id: "run_fm_in2", + project_id: "proj_other", + organization_id: "org_tenant1", + status: "PENDING", + }), + ]); - // Query using IN clause with external project_ref values - const [error, result] = await executeTSQL(client, { - name: "test-field-mapping-in", - query: - "SELECT run_id FROM task_runs WHERE project_ref IN ('my-project-ref', 'other-project')", - schema: z.object({ run_id: z.string() }), - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_tenant1" }, - }, - tableSchema: [fieldMappingSchema], - fieldMappings: { - project: { - proj_tenant1: "my-project-ref", - proj_other: "other-project", - }, + // Query using IN clause with external project_ref values + const [error, result] = await executeTSQL(client, { + name: "test-field-mapping-in", + query: + "SELECT run_id FROM task_runs WHERE project_ref IN ('my-project-ref', 'other-project')", + schema: z.object({ run_id: z.string() }), + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_tenant1" }, + }, + tableSchema: [fieldMappingSchema], + fieldMappings: { + project: { + proj_tenant1: "my-project-ref", + proj_other: "other-project", }, - }); + }, + }); - expect(error).toBeNull(); - expect(result?.rows).toHaveLength(2); - expect(result?.rows?.map((r) => r.run_id).sort()).toEqual(["run_fm_in1", "run_fm_in2"]); - } - ); + expect(error).toBeNull(); + expect(result?.rows).toHaveLength(2); + expect(result?.rows?.map((r) => r.run_id).sort()).toEqual(["run_fm_in1", "run_fm_in2"]); + }); }); diff --git a/internal-packages/compute/src/client.ts b/internal-packages/compute/src/client.ts index d6215678b5..bfcf5f782a 100644 --- a/internal-packages/compute/src/client.ts +++ b/internal-packages/compute/src/client.ts @@ -62,7 +62,11 @@ class HttpTransport { return options?.signal ?? AbortSignal.timeout(this.opts.timeoutMs); } - async post(path: string, body: unknown, options?: RequestOptions): Promise { + async post( + path: string, + body: unknown, + options?: RequestOptions + ): Promise { const url = `${this.opts.gatewayUrl}${path}`; const response = await fetch(url, { diff --git a/internal-packages/dashboard-agent-db/drizzle.config.ts b/internal-packages/dashboard-agent-db/drizzle.config.ts index ddaddb9fd8..59645c428e 100644 --- a/internal-packages/dashboard-agent-db/drizzle.config.ts +++ b/internal-packages/dashboard-agent-db/drizzle.config.ts @@ -3,9 +3,7 @@ import { defineConfig } from "drizzle-kit"; // Cloud points at the dedicated PlanetScale database; OSS falls back to the main // DATABASE_URL (tables still land in the trigger_dashboard_agent schema). const url = - process.env.DASHBOARD_AGENT_DATABASE_URL ?? - process.env.DATABASE_URL ?? - "postgres://placeholder"; // generate is offline; a real url is only needed for migrate/studio + process.env.DASHBOARD_AGENT_DATABASE_URL ?? process.env.DATABASE_URL ?? "postgres://placeholder"; // generate is offline; a real url is only needed for migrate/studio export default defineConfig({ schema: "./src/schema.ts", diff --git a/internal-packages/dashboard-agent-db/drizzle/meta/0000_snapshot.json b/internal-packages/dashboard-agent-db/drizzle/meta/0000_snapshot.json index c60365cd59..9ff7afa4f6 100644 --- a/internal-packages/dashboard-agent-db/drizzle/meta/0000_snapshot.json +++ b/internal-packages/dashboard-agent-db/drizzle/meta/0000_snapshot.json @@ -175,4 +175,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/internal-packages/dashboard-agent-db/drizzle/meta/0001_snapshot.json b/internal-packages/dashboard-agent-db/drizzle/meta/0001_snapshot.json index 5846087eb2..57f716b951 100644 --- a/internal-packages/dashboard-agent-db/drizzle/meta/0001_snapshot.json +++ b/internal-packages/dashboard-agent-db/drizzle/meta/0001_snapshot.json @@ -303,10 +303,7 @@ "compositePrimaryKeys": { "chat_turn_evals_chat_id_turn_pk": { "name": "chat_turn_evals_chat_id_turn_pk", - "columns": [ - "chat_id", - "turn" - ] + "columns": ["chat_id", "turn"] } }, "uniqueConstraints": {}, @@ -441,4 +438,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/internal-packages/dashboard-agent-db/drizzle/meta/_journal.json b/internal-packages/dashboard-agent-db/drizzle/meta/_journal.json index a3e444293e..62516c1d0e 100644 --- a/internal-packages/dashboard-agent-db/drizzle/meta/_journal.json +++ b/internal-packages/dashboard-agent-db/drizzle/meta/_journal.json @@ -17,4 +17,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/internal-packages/dashboard-agent-db/migrate-status.mjs b/internal-packages/dashboard-agent-db/migrate-status.mjs index 8d8208fdb2..2edf51c6bf 100644 --- a/internal-packages/dashboard-agent-db/migrate-status.mjs +++ b/internal-packages/dashboard-agent-db/migrate-status.mjs @@ -28,10 +28,7 @@ function normalizeConnectionString(value) { } } -const journalPath = join( - dirname(fileURLToPath(import.meta.url)), - "drizzle/meta/_journal.json" -); +const journalPath = join(dirname(fileURLToPath(import.meta.url)), "drizzle/meta/_journal.json"); const sql = postgres(normalizeConnectionString(connectionString), { max: 1, prepare: false, @@ -54,9 +51,7 @@ async function main() { } const pending = entries.filter((e) => e.when > lastAppliedAt); - console.log( - `${entries.length} migration(s) found, ${entries.length - pending.length} applied` - ); + console.log(`${entries.length} migration(s) found, ${entries.length - pending.length} applied`); if (pending.length > 0) { console.log(`${pending.length} pending migration(s):`); diff --git a/internal-packages/dashboard-agent-db/src/queries.ts b/internal-packages/dashboard-agent-db/src/queries.ts index a5cbe4ffa1..ca9ba1de54 100644 --- a/internal-packages/dashboard-agent-db/src/queries.ts +++ b/internal-packages/dashboard-agent-db/src/queries.ts @@ -1,6 +1,12 @@ import { and, desc, eq, isNull, sql } from "drizzle-orm"; import type { DashboardAgentDb } from "./client.js"; -import { chats, chatSessions, chatTurnEvals, type ChatSession, type NewChatTurnEval } from "./schema.js"; +import { + chats, + chatSessions, + chatTurnEvals, + type ChatSession, + type NewChatTurnEval, +} from "./schema.js"; /** * The access-pattern layer. Every query that touches user data is scoped by @@ -64,11 +70,7 @@ export async function getChatMessages( .select({ messages: chats.messages }) .from(chats) .where( - and( - eq(chats.id, params.chatId), - eq(chats.userId, params.userId), - isNull(chats.deletedAt) - ) + and(eq(chats.id, params.chatId), eq(chats.userId, params.userId), isNull(chats.deletedAt)) ) .limit(1); return rows[0]?.messages ?? null; @@ -175,11 +177,7 @@ export async function setChatTitleIfDefault( .update(chats) .set({ title: params.title, updatedAt: sql`now()` }) .where( - and( - eq(chats.id, params.chatId), - eq(chats.title, DEFAULT_CHAT_TITLE), - isNull(chats.deletedAt) - ) + and(eq(chats.id, params.chatId), eq(chats.title, DEFAULT_CHAT_TITLE), isNull(chats.deletedAt)) ); } diff --git a/internal-packages/dashboard-agent/src/dashboard-agent.eval.ts b/internal-packages/dashboard-agent/src/dashboard-agent.eval.ts index d02628dbe3..827066482f 100644 --- a/internal-packages/dashboard-agent/src/dashboard-agent.eval.ts +++ b/internal-packages/dashboard-agent/src/dashboard-agent.eval.ts @@ -195,7 +195,12 @@ const Verdict = z.object({ .min(1) .max(5) .describe("Does the answer directly address the user's question? 5 = fully, 1 = not at all."), - concise: z.number().int().min(1).max(5).describe("Direct and free of padding. Do not reward length."), + concise: z + .number() + .int() + .min(1) + .max(5) + .describe("Direct and free of padding. Do not reward length."), }); const JUDGE_SYSTEM = [ @@ -246,48 +251,43 @@ const TOOL_CASES: Array<{ question: string; expect: string }> = [ const TOOL_SELECTION_THRESHOLD = 0.83; // tolerate ~2/12 misses; a trend reds the suite describe.skipIf(!HAS_KEY)("dashboardAgent evals (real model)", () => { - it( - "tool selection: picks the right tool for the question", - async () => { - const results: Array<{ question: string; expected: string; got: string; ok: boolean }> = []; - for (const c of TOOL_CASES) { - const { calls } = await runCase(c.question); - const got = calls[0]?.tool ?? "(none)"; - results.push({ question: c.question, expected: c.expect, got, ok: got === c.expect }); - } + it("tool selection: picks the right tool for the question", async () => { + const results: Array<{ question: string; expected: string; got: string; ok: boolean }> = []; + for (const c of TOOL_CASES) { + const { calls } = await runCase(c.question); + const got = calls[0]?.tool ?? "(none)"; + results.push({ question: c.question, expected: c.expect, got, ok: got === c.expect }); + } - const passed = results.filter((r) => r.ok).length; - const rate = passed / results.length; - // Surface the full table so a failing case is diagnosable, not just a number. - // process.stdout.write (not console.log) so it survives vitest's console intercept. - process.stdout.write( - `\ntool selection: ${passed}/${results.length} (${(rate * 100).toFixed(0)}%)\n` + - results - .map((r) => ` ${r.ok ? "PASS" : "FAIL"} ${r.got.padEnd(18)} (want ${r.expected}) ${r.question}`) - .join("\n") + - "\n" - ); + const passed = results.filter((r) => r.ok).length; + const rate = passed / results.length; + // Surface the full table so a failing case is diagnosable, not just a number. + // process.stdout.write (not console.log) so it survives vitest's console intercept. + process.stdout.write( + `\ntool selection: ${passed}/${results.length} (${(rate * 100).toFixed(0)}%)\n` + + results + .map( + (r) => + ` ${r.ok ? "PASS" : "FAIL"} ${r.got.padEnd(18)} (want ${r.expected}) ${r.question}` + ) + .join("\n") + + "\n" + ); - expect(rate).toBeGreaterThanOrEqual(TOOL_SELECTION_THRESHOLD); - }, - 180_000 - ); + expect(rate).toBeGreaterThanOrEqual(TOOL_SELECTION_THRESHOLD); + }, 180_000); - it( - "answer quality: grounded and on-question (LLM judge)", - async () => { - const question = "What errors are happening in this environment? Summarize the top ones."; - const { calls, answer } = await runCase(question); + it("answer quality: grounded and on-question (LLM judge)", async () => { + const question = "What errors are happening in this environment? Summarize the top ones."; + const { calls, answer } = await runCase(question); - expect(calls[0]?.tool).toBe("list_errors"); - expect(answer.length).toBeGreaterThan(0); + expect(calls[0]?.tool).toBe("list_errors"); + expect(answer.length).toBeGreaterThan(0); - const verdict = await judge({ question, toolData: FIXTURES.list_errors, answer }); - process.stdout.write(`\nanswer:\n${answer}\n\njudge: ${JSON.stringify(verdict)}\n`); + const verdict = await judge({ question, toolData: FIXTURES.list_errors, answer }); + process.stdout.write(`\nanswer:\n${answer}\n\njudge: ${JSON.stringify(verdict)}\n`); - expect(verdict.grounded).toBeGreaterThanOrEqual(4); - expect(verdict.answersQuestion).toBeGreaterThanOrEqual(4); - }, - 120_000 - ); + expect(verdict.grounded).toBeGreaterThanOrEqual(4); + expect(verdict.answersQuestion).toBeGreaterThanOrEqual(4); + }, 120_000); }); diff --git a/internal-packages/dashboard-agent/src/dashboard-agent.test.ts b/internal-packages/dashboard-agent/src/dashboard-agent.test.ts index ad2f09da3d..db23d2130f 100644 --- a/internal-packages/dashboard-agent/src/dashboard-agent.test.ts +++ b/internal-packages/dashboard-agent/src/dashboard-agent.test.ts @@ -302,7 +302,9 @@ describe("buildDashboardAgentTools", () => { category: "user_code_error", likelyCause: "processOrder throws when items is empty.", confidence: "high", - evidence: [{ type: "error", detail: "Error: order has no items", reference: "run_abc123" }], + evidence: [ + { type: "error", detail: "Error: order has no items", reference: "run_abc123" }, + ], nextSteps: ["Validate the payload before triggering."], }, ], diff --git a/internal-packages/dashboard-agent/src/dashboard-agent.ts b/internal-packages/dashboard-agent/src/dashboard-agent.ts index 4639e7095c..ce1919e68e 100644 --- a/internal-packages/dashboard-agent/src/dashboard-agent.ts +++ b/internal-packages/dashboard-agent/src/dashboard-agent.ts @@ -45,8 +45,7 @@ let dbClient: DashboardAgentDbClient | undefined; function getDb(): DashboardAgentDbClient { if (!dbClient) { - const connectionString = - process.env.DASHBOARD_AGENT_DATABASE_URL ?? process.env.DATABASE_URL; + const connectionString = process.env.DASHBOARD_AGENT_DATABASE_URL ?? process.env.DATABASE_URL; if (!connectionString) { throw new Error( "DASHBOARD_AGENT_DATABASE_URL (or DATABASE_URL) must be set for the dashboard agent" @@ -182,7 +181,9 @@ async function generateAndSaveTitle( const { text } = await generateText({ model: locals.get(dashboardAgentModelKey) ?? - registry.languageModel((resolved.model ?? "anthropic:claude-haiku-4-5") as `anthropic:${string}`), + registry.languageModel( + (resolved.model ?? "anthropic:claude-haiku-4-5") as `anthropic:${string}` + ), system: resolved.text, prompt: userText, ...resolved.toAISDKTelemetry(), @@ -373,7 +374,9 @@ export const dashboardAgent = chat.agent({ // prompt's model through the provider registry. model: locals.get(dashboardAgentModelKey) ?? - registry.languageModel((resolved.model ?? "anthropic:claude-sonnet-4-6") as `anthropic:${string}`), + registry.languageModel( + (resolved.model ?? "anthropic:claude-sonnet-4-6") as `anthropic:${string}` + ), messages, abortSignal: signal, // toStreamTextOptions() defaults to a single step; override so the model diff --git a/internal-packages/dashboard-agent/src/eval-turn.ts b/internal-packages/dashboard-agent/src/eval-turn.ts index ef86e7cdc4..3943c7726c 100644 --- a/internal-packages/dashboard-agent/src/eval-turn.ts +++ b/internal-packages/dashboard-agent/src/eval-turn.ts @@ -25,10 +25,11 @@ const JUDGE_MODEL = "claude-sonnet-4-6"; let dbClient: DashboardAgentDbClient | undefined; function getEvalDb(): DashboardAgentDbClient { if (!dbClient) { - const connectionString = - process.env.DASHBOARD_AGENT_DATABASE_URL ?? process.env.DATABASE_URL; + const connectionString = process.env.DASHBOARD_AGENT_DATABASE_URL ?? process.env.DATABASE_URL; if (!connectionString) { - throw new Error("DASHBOARD_AGENT_DATABASE_URL (or DATABASE_URL) must be set for the eval task"); + throw new Error( + "DASHBOARD_AGENT_DATABASE_URL (or DATABASE_URL) must be set for the eval task" + ); } dbClient = createDashboardAgentDb(connectionString, { max: 2 }); } @@ -77,8 +78,15 @@ const TurnEval = z.object({ .int() .min(1) .max(5) - .describe("Does the answer use only facts from the tool results? Penalize invented ids/counts/status. 5 = fully grounded."), - answered: z.number().int().min(1).max(5).describe("Does it directly answer the question? 5 = fully."), + .describe( + "Does the answer use only facts from the tool results? Penalize invented ids/counts/status. 5 = fully grounded." + ), + answered: z + .number() + .int() + .min(1) + .max(5) + .describe("Does it directly answer the question? 5 = fully."), concise: z.number().int().min(1).max(5).describe("Direct, no padding. Do not reward length."), // Insight classification. intentCategory: z @@ -86,17 +94,25 @@ const TurnEval = z.object({ .describe("What the user was trying to do."), outcome: z .enum(["resolved", "partial", "unresolved", "deflected"]) - .describe("Did the agent actually help? deflected = sent the user elsewhere without answering."), + .describe( + "Did the agent actually help? deflected = sent the user elsewhere without answering." + ), sentiment: z.enum(["positive", "neutral", "negative", "frustrated"]), capabilityGap: z .boolean() .describe("The agent lacked a tool, data, or permission needed to fully help."), - docsGap: z.boolean().describe("A how-to the agent answered weakly or that better docs would solve."), + docsGap: z + .boolean() + .describe("A how-to the agent answered weakly or that better docs would solve."), supportOpportunity: z .boolean() - .describe("The user seems stuck, blocked, or frustrated and would benefit from a human follow-up."), + .describe( + "The user seems stuck, blocked, or frustrated and would benefit from a human follow-up." + ), featureRequest: z.boolean().describe("The user wants something the product does not do."), - topics: z.array(z.string()).describe("1-3 short topic tags, e.g. 'concurrency', 'failed deploys'."), + topics: z + .array(z.string()) + .describe("1-3 short topic tags, e.g. 'concurrency', 'failed deploys'."), signals: z .array( z.object({ diff --git a/internal-packages/dashboard-agent/src/prompts.ts b/internal-packages/dashboard-agent/src/prompts.ts index 1f0a152599..3e8a46dec9 100644 --- a/internal-packages/dashboard-agent/src/prompts.ts +++ b/internal-packages/dashboard-agent/src/prompts.ts @@ -28,7 +28,8 @@ export const systemPrompt = prompts.define({ // repo, so the agent has the source-reading tools too. export const codeSystemPrompt = prompts.define({ id: "dashboard-agent-system-code", - description: "System prompt for the in-dashboard agent when the project's GitHub repo is connected.", + description: + "System prompt for the in-dashboard agent when the project's GitHub repo is connected.", model: `anthropic:${DASHBOARD_AGENT_MODEL}`, content: DASHBOARD_AGENT_CODE_SYSTEM_PROMPT, }); diff --git a/internal-packages/dashboard-agent/src/repo-tools.test.ts b/internal-packages/dashboard-agent/src/repo-tools.test.ts index d00018ed71..d56d15580a 100644 --- a/internal-packages/dashboard-agent/src/repo-tools.test.ts +++ b/internal-packages/dashboard-agent/src/repo-tools.test.ts @@ -67,7 +67,12 @@ afterAll(async () => { describe("repo-tools", () => { it("get_repo_info returns the connected repo and pinned commit", async () => { const res = await call(tools.get_repo_info, {}); - expect(res).toEqual({ owner: "acme", repo: "demo", sha: "deadbeefdeadbeef", defaultBranch: "main" }); + expect(res).toEqual({ + owner: "acme", + repo: "demo", + sha: "deadbeefdeadbeef", + defaultBranch: "main", + }); }); it("read_file reads a file from the workspace", async () => { @@ -78,7 +83,11 @@ describe("repo-tools", () => { }); it("read_file honors a line range", async () => { - const res: any = await call(tools.read_file, { path: "src/trigger/order.ts", startLine: 2, endLine: 2 }); + const res: any = await call(tools.read_file, { + path: "src/trigger/order.ts", + startLine: 2, + endLine: 2, + }); expect(res.content).toBe("const LIMIT = 10000;"); expect(res.startLine).toBe(2); expect(res.endLine).toBe(2); @@ -99,7 +108,10 @@ describe("repo-tools", () => { it("read_file with runId reads the run's pinned commit", async () => { const def: any = await call(tools.read_file, { path: "src/trigger/order.ts" }); expect(def.content).toContain("const LIMIT = 10000;"); - const pinned: any = await call(tools.read_file, { path: "src/trigger/order.ts", runId: "run_pinned" }); + const pinned: any = await call(tools.read_file, { + path: "src/trigger/order.ts", + runId: "run_pinned", + }); expect(pinned.error).toBeUndefined(); expect(pinned.content).toContain("const LIMIT = 5000;"); }); @@ -110,14 +122,19 @@ describe("repo-tools", () => { }); it("read_file with an unresolvable runId errors instead of falling back", async () => { - const res: any = await call(tools.read_file, { path: "src/trigger/order.ts", runId: "run_unknown" }); + const res: any = await call(tools.read_file, { + path: "src/trigger/order.ts", + runId: "run_unknown", + }); expect(res.error).toMatch(/Couldn't resolve the source/); }); it.runIf(hasRg)("search_code finds a match (and does not hang on stdin)", async () => { const res: any = await call(tools.search_code, { query: "const LIMIT" }); expect(res.error).toBeUndefined(); - expect(res.matches.some((m: any) => String(m.file).includes("order.ts") && /LIMIT/.test(m.text))).toBe(true); + expect( + res.matches.some((m: any) => String(m.file).includes("order.ts") && /LIMIT/.test(m.text)) + ).toBe(true); }); it.runIf(hasRg)("list_files lists workspace files", async () => { diff --git a/internal-packages/dashboard-agent/src/repo-tools.ts b/internal-packages/dashboard-agent/src/repo-tools.ts index 552996e9d9..910f60df51 100644 --- a/internal-packages/dashboard-agent/src/repo-tools.ts +++ b/internal-packages/dashboard-agent/src/repo-tools.ts @@ -86,7 +86,8 @@ async function ensureWorkspace(snapshot: RepoSnapshot): Promise { const length = Number(res.headers.get("content-length") ?? 0); if (length > MAX_ARCHIVE_BYTES) throw new Error(`archive too large (${length} bytes)`); const bytes = new Uint8Array(await res.arrayBuffer()); - if (bytes.length > MAX_ARCHIVE_BYTES) throw new Error(`archive too large (${bytes.length} bytes)`); + if (bytes.length > MAX_ARCHIVE_BYTES) + throw new Error(`archive too large (${bytes.length} bytes)`); const scratch = await mkdtemp(join(tmpdir(), "dashboard-agent-tar-")); tarPath = join(scratch, "repo.tar.gz"); @@ -149,7 +150,8 @@ export function buildRepoTools( // (resolved server-side), otherwise the default tracked-branch snapshot. async function snapshotFor(runId?: string): Promise { if (!runId) return defaultSnapshot; - if (!resolveRunSnapshot) return { error: "Reading a specific run's source isn't available here." }; + if (!resolveRunSnapshot) + return { error: "Reading a specific run's source isn't available here." }; const snap = await resolveRunSnapshot(runId); return ( snap ?? { @@ -177,7 +179,12 @@ export function buildRepoTools( execute: async ({ runId }) => { const snap = await snapshotFor(runId); if ("error" in snap) return snap; - return { owner: snap.owner, repo: snap.repo, sha: snap.sha, defaultBranch: snap.defaultBranch }; + return { + owner: snap.owner, + repo: snap.repo, + sha: snap.sha, + defaultBranch: snap.defaultBranch, + }; }, }), @@ -199,8 +206,14 @@ export function buildRepoTools( const cwd = realSub ?? sub; try { const { stdout } = await execFileAsync("rg", args, { cwd, maxBuffer: 16 * 1024 * 1024 }); - const files = stdout.split("\n").filter(Boolean).map((f) => relative(workdir, resolve(cwd, f))); - return { files: files.slice(0, MAX_LIST_FILES), truncated: files.length > MAX_LIST_FILES }; + const files = stdout + .split("\n") + .filter(Boolean) + .map((f) => relative(workdir, resolve(cwd, f))); + return { + files: files.slice(0, MAX_LIST_FILES), + truncated: files.length > MAX_LIST_FILES, + }; } catch (error) { // rg exits 1 when there are no matches; treat as empty, not an error. if ((error as { code?: number }).code === 1) return { files: [], truncated: false }; @@ -256,14 +269,19 @@ export function buildRepoTools( // in a spawned process) and blocks forever. The "." makes it search files. args.push("-e", query, "."); try { - const { stdout } = await execFileAsync("rg", args, { cwd: workdir, maxBuffer: 16 * 1024 * 1024 }); + const { stdout } = await execFileAsync("rg", args, { + cwd: workdir, + maxBuffer: 16 * 1024 * 1024, + }); const matches = stdout .split("\n") .filter(Boolean) .slice(0, cap) .map((line) => { const m = line.match(/^([^:]+):(\d+):(.*)$/); - return m ? { file: m[1], line: Number(m[2]), text: m[3].slice(0, 300) } : { text: line }; + return m + ? { file: m[1], line: Number(m[2]), text: m[3].slice(0, 300) } + : { text: line }; }); return { matches, truncated: matches.length >= cap }; } catch (error) { diff --git a/internal-packages/dashboard-agent/src/tool-schemas.ts b/internal-packages/dashboard-agent/src/tool-schemas.ts index 8117e3c563..65a7c0c60a 100644 --- a/internal-packages/dashboard-agent/src/tool-schemas.ts +++ b/internal-packages/dashboard-agent/src/tool-schemas.ts @@ -48,12 +48,20 @@ export const listRunsSchema = tool({ errorId: z .string() .optional() - .describe("Only runs that hit this error group (an error_... id from list_errors/get_error)."), + .describe( + "Only runs that hit this error group (an error_... id from list_errors/get_error)." + ), period: z .string() .optional() .describe("Relative window, e.g. 1h, 24h, 7d. Max 30d; larger values are capped at 30d."), - limit: z.number().int().positive().max(50).optional().describe("Max runs to return (default 10)."), + limit: z + .number() + .int() + .positive() + .max(50) + .optional() + .describe("Max runs to return (default 10)."), }), }); @@ -119,7 +127,9 @@ export const getQuerySchemaSchema = tool({ table: z .string() .optional() - .describe("A table name (e.g. 'runs') to get its columns. Omit to list the available tables."), + .describe( + "A table name (e.g. 'runs') to get its columns. Omit to list the available tables." + ), }), }); @@ -129,11 +139,15 @@ export const runQuerySchema = tool({ inputSchema: z.object({ query: z .string() - .describe("The TRQL query. A read-only SELECT over runs / metrics / llm_metrics / llm_models."), + .describe( + "The TRQL query. A read-only SELECT over runs / metrics / llm_metrics / llm_models." + ), period: z .string() .optional() - .describe("Time window shorthand like '24h', '7d', '30d' (max 30d), applied to the table's time column."), + .describe( + "Time window shorthand like '24h', '7d', '30d' (max 30d), applied to the table's time column." + ), }), }); @@ -141,7 +155,9 @@ export const askSupportSchema = tool({ description: "Ask the Trigger.dev support assistant a question about how Trigger.dev works: docs, concepts, features, configuration, best practices, and troubleshooting how-tos (e.g. 'how do retries work?', 'how do I set a concurrency limit?', 'does Trigger.dev support cron schedules?'). Use this for product/knowledge questions, NOT for the user's own runs, errors, or data (use the read and query tools for those). Returns a composed answer.", inputSchema: z.object({ - question: z.string().describe("The user's question about how Trigger.dev works, in natural language."), + question: z + .string() + .describe("The user's question about how Trigger.dev works, in natural language."), }), }); @@ -183,7 +199,9 @@ export const diagnosisBlockSchema = z.object({ .describe("Your classification of the root cause."), likelyCause: z .string() - .describe("The most probable root cause, in specific terms β€” name the code, config, or dependency."), + .describe( + "The most probable root cause, in specific terms β€” name the code, config, or dependency." + ), confidence: z .enum(["high", "medium", "low"]) .describe("How confident you are in this diagnosis given the evidence. Be honest."), @@ -208,7 +226,9 @@ export const diagnosisBlockSchema = z.object({ ), }) ) - .describe("The concrete signals behind the diagnosis. Cite real ids, spans, versions, or file:line."), + .describe( + "The concrete signals behind the diagnosis. Cite real ids, spans, versions, or file:line." + ), impact: z .string() .optional() @@ -220,7 +240,9 @@ export const diagnosisBlockSchema = z.object({ label: z.string().describe("Button text, e.g. 'View run' or 'Read the retries docs'."), kind: z .enum(["view_run", "docs"]) - .describe("view_run links to a run page in this environment; docs opens an external URL."), + .describe( + "view_run links to a run page in this environment; docs opens an external URL." + ), target: z.string().describe("For view_run: a run id (run_...). For docs: an https URL."), }) ) @@ -244,13 +266,19 @@ export const chartBlockSchema = z.object({ period: z .string() .optional() - .describe("Time window shorthand like '24h', '7d', '30d' (max 30d), applied to the table's time column."), + .describe( + "Time window shorthand like '24h', '7d', '30d' (max 30d), applied to the table's time column." + ), chartType: z .enum(["line", "bar"]) - .describe("line for trends over time, bar for comparing categories. Stack with `stacked` for composition."), + .describe( + "line for trends over time, bar for comparing categories. Stack with `stacked` for composition." + ), xAxisColumn: z .string() - .describe("The result column for the x-axis: a time bucket (for line) or a category (for bar)."), + .describe( + "The result column for the x-axis: a time bucket (for line) or a category (for bar)." + ), yAxisColumns: z .array(z.string()) .min(1) @@ -258,15 +286,23 @@ export const chartBlockSchema = z.object({ groupByColumn: z .string() .nullish() - .describe("Optional result column to split a single yAxisColumn into one series per distinct value."), - stacked: z.boolean().optional().describe("Stack the series (cumulative/composition). Default false."), + .describe( + "Optional result column to split a single yAxisColumn into one series per distinct value." + ), + stacked: z + .boolean() + .optional() + .describe("Stack the series (cumulative/composition). Default false."), aggregation: z .enum(["sum", "avg", "count", "min", "max"]) .optional() .describe("How to combine values that share an x point. Default sum."), }); -export const viewBlockSchema = z.discriminatedUnion("type", [diagnosisBlockSchema, chartBlockSchema]); +export const viewBlockSchema = z.discriminatedUnion("type", [ + diagnosisBlockSchema, + chartBlockSchema, +]); export type DiagnosisBlock = z.infer; export type ChartBlock = z.infer; @@ -303,7 +339,10 @@ export const listFilesSchema = tool({ "List source files in the connected repository (respecting .gitignore). Optionally filter by a glob like '**/*.ts' or scope to a subdirectory. Use this to find where something lives before reading it.", inputSchema: z.object({ glob: z.string().optional().describe("Glob filter, e.g. 'src/**/*.ts' or '*.json'."), - path: z.string().optional().describe("Subdirectory (relative to repo root) to scope the listing to."), + path: z + .string() + .optional() + .describe("Subdirectory (relative to repo root) to scope the listing to."), runId: runIdField, }), }); @@ -312,7 +351,9 @@ export const readFileSchema = tool({ description: "Read a file from the connected repository by its path relative to the repo root. Optionally restrict to a line range. Use this to read the actual task source behind a run or error.", inputSchema: z.object({ - path: z.string().describe("File path relative to the repo root, e.g. src/trigger/processOrder.ts."), + path: z + .string() + .describe("File path relative to the repo root, e.g. src/trigger/processOrder.ts."), startLine: z.number().int().positive().optional().describe("First line to include (1-based)."), endLine: z.number().int().positive().optional().describe("Last line to include (1-based)."), runId: runIdField, @@ -325,7 +366,13 @@ export const searchCodeSchema = tool({ inputSchema: z.object({ query: z.string().describe("The ripgrep pattern to search for."), glob: z.string().optional().describe("Restrict the search to files matching this glob."), - maxResults: z.number().int().positive().max(80).optional().describe("Max matches to return (default 40)."), + maxResults: z + .number() + .int() + .positive() + .max(80) + .optional() + .describe("Max matches to return (default 40)."), runId: runIdField, }), }); diff --git a/internal-packages/dashboard-agent/src/tools.ts b/internal-packages/dashboard-agent/src/tools.ts index 16988cde66..4106ef03ee 100644 --- a/internal-packages/dashboard-agent/src/tools.ts +++ b/internal-packages/dashboard-agent/src/tools.ts @@ -174,7 +174,11 @@ function curateTrace(data: unknown) { for (const child of span.children ?? []) walk(child, depth + 1); }; walk(root, 0); - return { traceId: (data as any)?.trace?.traceId, spans, truncated: spans.length >= MAX_TRACE_SPANS }; + return { + traceId: (data as any)?.trace?.traceId, + spans, + truncated: spans.length >= MAX_TRACE_SPANS, + }; } function curateErrors(data: unknown) { @@ -260,7 +264,13 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe if (!result.ok) return null; const d = result.data as Partial | undefined; if (!d?.tarballUrl || !d.owner || !d.repo || !d.sha) return null; - return { tarballUrl: d.tarballUrl, owner: d.owner, repo: d.repo, sha: d.sha, defaultBranch: d.defaultBranch }; + return { + tarballUrl: d.tarballUrl, + owner: d.owner, + repo: d.repo, + sha: d.sha, + defaultBranch: d.defaultBranch, + }; }; const apiTools: ToolSet = { @@ -280,7 +290,11 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe if (!hasAuth) return NO_AUTH; const ref = inputRef ?? projectRef; if (!ref) return { error: "No project ref available. Ask the user which project." }; - const result = await apiGet(origin, `/api/v1/projects/${ref}/environments`, userActorToken!); + const result = await apiGet( + origin, + `/api/v1/projects/${ref}/environments`, + userActorToken! + ); if (!result.ok) return { error: `Couldn't list environments (status ${result.status}).` }; return curateEnvironments(result.data); }, @@ -340,7 +354,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe const envJwt = await getEnvJwt(); if (!envJwt) return { error: "No current environment is available to read runs from." }; const result = await apiGet(origin, `/api/v1/runs/${runId}/trace`, envJwt); - if (!result.ok) return { error: `Couldn't get the trace for ${runId} (status ${result.status}).` }; + if (!result.ok) + return { error: `Couldn't get the trace for ${runId} (status ${result.status}).` }; return curateTrace(result.data); }, }), @@ -368,7 +383,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe const envJwt = await getEnvJwt(); if (!envJwt) return { error: "No current environment is available to read errors from." }; const result = await apiGet(origin, `/api/v1/errors/${errorId}`, envJwt); - if (!result.ok) return { error: `Couldn't get error ${errorId} (status ${result.status}).` }; + if (!result.ok) + return { error: `Couldn't get error ${errorId} (status ${result.status}).` }; return curateError(result.data); }, }), @@ -379,7 +395,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe const envJwt = await getEnvJwt(); if (!envJwt) return { error: "No current environment is available to query." }; const result = await apiGet(origin, "/api/v1/query/schema", envJwt); - if (!result.ok) return { error: `Couldn't load the query schema (status ${result.status}).` }; + if (!result.ok) + return { error: `Couldn't load the query schema (status ${result.status}).` }; const tables = ((result.data as { tables?: any[] })?.tables ?? []) as any[]; // No table β†’ list what's queryable; a table β†’ its columns. if (!table) { @@ -393,7 +410,9 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe } const match = tables.find((t) => t.name === table); if (!match) { - return { error: `Unknown table "${table}". Available: ${tables.map((t) => t.name).join(", ")}.` }; + return { + error: `Unknown table "${table}". Available: ${tables.map((t) => t.name).join(", ")}.`, + }; } return { name: match.name, @@ -433,7 +452,9 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe // the model can fix the query rather than the turn dying. const data = (await res.json().catch(() => ({}))) as { results?: unknown; error?: string }; if (!res.ok) return { error: data.error ?? `Query failed (status ${res.status}).` }; - const rows = Array.isArray(data.results) ? (data.results as Array>) : []; + const rows = Array.isArray(data.results) + ? (data.results as Array>) + : []; const cap = 200; return { rows: rows.slice(0, cap), rowCount: rows.length, truncated: rows.length > cap }; }, @@ -448,7 +469,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe execute: async ({ question }) => { const url = process.env.SUPPORT_ASK_URL ?? "http://localhost:3939/api/ask"; const secret = process.env.SUPPORT_ASK_SECRET; - if (!secret) return { error: "The support assistant isn't configured in this environment." }; + if (!secret) + return { error: "The support assistant isn't configured in this environment." }; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 60_000); try { @@ -461,7 +483,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe }), signal: controller.signal, }); - if (!res.ok) return { error: `The support assistant request failed (status ${res.status}).` }; + if (!res.ok) + return { error: `The support assistant request failed (status ${res.status}).` }; // The endpoint streams a UI-message SSE; accumulate the text-delta // chunks into the final answer (tool-output-error chunks are noise). const body = await res.text(); @@ -472,7 +495,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe if (!payload || payload === "[DONE]") continue; try { const chunk = JSON.parse(payload) as { type?: string; delta?: string }; - if (chunk.type === "text-delta" && typeof chunk.delta === "string") answer += chunk.delta; + if (chunk.type === "text-delta" && typeof chunk.delta === "string") + answer += chunk.delta; } catch { // Skip keepalives / non-JSON lines. } diff --git a/internal-packages/database/package.json b/internal-packages/database/package.json index b8e530908e..ec0bc950b8 100644 --- a/internal-packages/database/package.json +++ b/internal-packages/database/package.json @@ -25,4 +25,4 @@ "build": "pnpm run clean && tsc --noEmit false --outDir dist --declaration", "dev": "tsc --noEmit false --outDir dist --declaration --watch" } -} \ No newline at end of file +} diff --git a/internal-packages/emails/emails/mfa-enabled.tsx b/internal-packages/emails/emails/mfa-enabled.tsx index 93db74128c..93af4b08aa 100644 --- a/internal-packages/emails/emails/mfa-enabled.tsx +++ b/internal-packages/emails/emails/mfa-enabled.tsx @@ -39,8 +39,7 @@ export default function Email(props: MfaEnabledEmailProps) { β€’ Keep your authenticator app safe and secured -
- β€’ Never share your MFA codes with anyone +
β€’ Never share your MFA codes with anyone
β€’ Store your recovery codes in a secure location
{ + async send({ to, from, replyTo, subject, react }: MailMessage): Promise { try { await this.#client.sendMail({ from: from, @@ -27,8 +27,7 @@ export class AwsSesMailTransport implements MailTransport { subject, html: await render(react), }); - } - catch (error) { + } catch (error) { if (error instanceof Error) { console.error( `Failed to send email to ${to}, ${subject}. Error ${error.name}: ${error.message}` @@ -40,7 +39,7 @@ export class AwsSesMailTransport implements MailTransport { } } - async sendPlainText({to, from, replyTo, subject, text}: PlainTextMailMessage): Promise { + async sendPlainText({ to, from, replyTo, subject, text }: PlainTextMailMessage): Promise { try { await this.#client.sendMail({ from: from, @@ -49,8 +48,7 @@ export class AwsSesMailTransport implements MailTransport { subject, text: text, }); - } - catch (error) { + } catch (error) { if (error instanceof Error) { console.error( `Failed to send email to ${to}, ${subject}. Error ${error.name}: ${error.message}` diff --git a/internal-packages/emails/src/transports/index.ts b/internal-packages/emails/src/transports/index.ts index af85ab6ee1..5e7f9497e6 100644 --- a/internal-packages/emails/src/transports/index.ts +++ b/internal-packages/emails/src/transports/index.ts @@ -18,7 +18,7 @@ export type PlainTextMailMessage = { replyTo: string; subject: string; text: string; -} +}; export interface MailTransport { send(message: MailMessage): Promise; @@ -33,13 +33,13 @@ export class EmailError extends Error { } export type MailTransportOptions = - AwsSesMailTransportOptions | - ResendMailTransportOptions | - NullMailTransportOptions | - SmtpMailTransportOptions + | AwsSesMailTransportOptions + | ResendMailTransportOptions + | NullMailTransportOptions + | SmtpMailTransportOptions; export function constructMailTransport(options: MailTransportOptions): MailTransport { - switch(options.type) { + switch (options.type) { case "aws-ses": return new AwsSesMailTransport(options); case "resend": diff --git a/internal-packages/emails/src/transports/null.ts b/internal-packages/emails/src/transports/null.ts index 6a78fb9013..3674410bf6 100644 --- a/internal-packages/emails/src/transports/null.ts +++ b/internal-packages/emails/src/transports/null.ts @@ -2,14 +2,13 @@ import { render } from "@react-email/render"; import { MailMessage, MailTransport, PlainTextMailMessage } from "./index"; export type NullMailTransportOptions = { - type: undefined, -} + type: undefined; +}; export class NullMailTransport implements MailTransport { - constructor(options: NullMailTransportOptions) { - } + constructor(options: NullMailTransportOptions) {} - async send({to, subject, react}: MailMessage): Promise { + async send({ to, subject, react }: MailMessage): Promise { const plainText = await render(react, { plainText: true, }); @@ -21,7 +20,7 @@ ${plainText} `); } - async sendPlainText({to, subject, text}: PlainTextMailMessage): Promise { + async sendPlainText({ to, subject, text }: PlainTextMailMessage): Promise { console.log(` ##### sendEmail to ${to}, subject: ${subject} diff --git a/internal-packages/emails/src/transports/resend.ts b/internal-packages/emails/src/transports/resend.ts index 9241dd4b93..534df99752 100644 --- a/internal-packages/emails/src/transports/resend.ts +++ b/internal-packages/emails/src/transports/resend.ts @@ -2,20 +2,20 @@ import { EmailError, MailMessage, MailTransport, PlainTextMailMessage } from "./ import { Resend } from "resend"; export type ResendMailTransportOptions = { - type: 'resend', + type: "resend"; config: { - apiKey?: string - } -} + apiKey?: string; + }; +}; export class ResendMailTransport implements MailTransport { #client: Resend; constructor(options: ResendMailTransportOptions) { - this.#client = new Resend(options.config.apiKey) + this.#client = new Resend(options.config.apiKey); } - async send({to, from, replyTo, subject, react}: MailMessage): Promise { + async send({ to, from, replyTo, subject, react }: MailMessage): Promise { const result = await this.#client.emails.send({ from: from, to, @@ -32,7 +32,7 @@ export class ResendMailTransport implements MailTransport { } } - async sendPlainText({to, from, replyTo, subject, text}: PlainTextMailMessage): Promise { + async sendPlainText({ to, from, replyTo, subject, text }: PlainTextMailMessage): Promise { const result = await this.#client.emails.send({ from: from, to, diff --git a/internal-packages/emails/tsconfig.json b/internal-packages/emails/tsconfig.json index fabe2cd474..6d7e4a1b48 100644 --- a/internal-packages/emails/tsconfig.json +++ b/internal-packages/emails/tsconfig.json @@ -13,7 +13,7 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "target": "ES2015", + "target": "ES2015" }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] diff --git a/internal-packages/llm-model-catalog/scripts/generate.mjs b/internal-packages/llm-model-catalog/scripts/generate.mjs index 115643b1df..430a4edb34 100644 --- a/internal-packages/llm-model-catalog/scripts/generate.mjs +++ b/internal-packages/llm-model-catalog/scripts/generate.mjs @@ -43,7 +43,8 @@ if (existsSync(pricesJsonPath)) { let out = 'import type { DefaultModelDefinition } from "./types.js";\n\n'; out += "// Auto-generated from default-model-prices.json β€” do not edit manually.\n"; - out += "// Run `pnpm run sync-prices` to update the JSON, then `pnpm run generate` to regenerate.\n"; + out += + "// Run `pnpm run sync-prices` to update the JSON, then `pnpm run generate` to regenerate.\n"; out += "// Source: https://github.com/langfuse/langfuse\n\n"; out += "export const defaultModelPrices: DefaultModelDefinition[] = "; out += JSON.stringify(stripped, null, 2) + ";\n"; @@ -65,9 +66,12 @@ if (existsSync(catalogJsonPath)) { for (const key of Object.keys(data)) { if (data[key].releaseDate === undefined) data[key].releaseDate = null; if (data[key].isHidden === undefined) data[key].isHidden = false; - if (data[key].supportsStructuredOutput === undefined) data[key].supportsStructuredOutput = false; - if (data[key].supportsParallelToolCalls === undefined) data[key].supportsParallelToolCalls = false; - if (data[key].supportsStreamingToolCalls === undefined) data[key].supportsStreamingToolCalls = false; + if (data[key].supportsStructuredOutput === undefined) + data[key].supportsStructuredOutput = false; + if (data[key].supportsParallelToolCalls === undefined) + data[key].supportsParallelToolCalls = false; + if (data[key].supportsStreamingToolCalls === undefined) + data[key].supportsStreamingToolCalls = false; if (data[key].deprecationDate === undefined) data[key].deprecationDate = null; if (data[key].knowledgeCutoff === undefined) data[key].knowledgeCutoff = null; if (data[key].resolvedAt === undefined) data[key].resolvedAt = new Date().toISOString(); @@ -82,7 +86,8 @@ if (existsSync(catalogJsonPath)) { let out = 'import type { ModelCatalogEntry } from "./types.js";\n\n'; out += "// Auto-generated from model-catalog.json β€” do not edit manually.\n"; - out += "// Run `pnpm run generate-catalog` to update the JSON, then `pnpm run generate` to regenerate.\n\n"; + out += + "// Run `pnpm run generate-catalog` to update the JSON, then `pnpm run generate` to regenerate.\n\n"; out += "export const modelCatalog: Record = "; out += JSON.stringify(data, null, 2) + ";\n"; diff --git a/internal-packages/llm-model-catalog/src/defaultPrices.ts b/internal-packages/llm-model-catalog/src/defaultPrices.ts index fb347c2bef..982b6b2ec1 100644 --- a/internal-packages/llm-model-catalog/src/defaultPrices.ts +++ b/internal-packages/llm-model-catalog/src/defaultPrices.ts @@ -6,3091 +6,3106 @@ import type { DefaultModelDefinition } from "./types.js"; export const defaultModelPrices: DefaultModelDefinition[] = [ { - "modelName": "gpt-4o", - "matchPattern": "(?i)^(openai/)?(gpt-4o)$", - "startDate": "2024-05-13T23:15:07.670Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "input_cached_tokens": 0.00000125, - "input_cache_read": 0.00000125, - "output": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-4o-2024-05-13", - "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-05-13)$", - "startDate": "2024-05-13T23:15:07.670Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000005, - "output": 0.000015 - } - } - ] - }, - { - "modelName": "gpt-4-1106-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4-1106-preview)$", - "startDate": "2024-04-23T10:37:17.092Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "gpt-4-turbo-vision", - "matchPattern": "(?i)^(openai/)?(gpt-4(-\\d{4})?-vision-preview)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "gpt-4-32k", - "matchPattern": "(?i)^(openai/)?(gpt-4-32k)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00006, - "output": 0.00012 - } - } - ] - }, - { - "modelName": "gpt-4-32k-0613", - "matchPattern": "(?i)^(openai/)?(gpt-4-32k-0613)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00006, - "output": 0.00012 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-1106", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-1106)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000001, - "output": 0.000002 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-0613", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0613)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000015, - "output": 0.000002 - } - } - ] - }, - { - "modelName": "gpt-4-0613", - "matchPattern": "(?i)^(openai/)?(gpt-4-0613)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00003, - "output": 0.00006 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-instruct", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-instruct)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000015, - "output": 0.000002 - } - } - ] - }, - { - "modelName": "text-ada-001", - "matchPattern": "(?i)^(text-ada-001)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 0.000004 - } - } - ] - }, - { - "modelName": "text-babbage-001", - "matchPattern": "(?i)^(text-babbage-001)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 5e-7 - } - } - ] - }, - { - "modelName": "text-curie-001", - "matchPattern": "(?i)^(text-curie-001)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 0.00002 - } - } - ] - }, - { - "modelName": "text-davinci-001", - "matchPattern": "(?i)^(text-davinci-001)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 0.00002 - } - } - ] - }, - { - "modelName": "text-davinci-002", - "matchPattern": "(?i)^(text-davinci-002)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 0.00002 - } - } - ] - }, - { - "modelName": "text-davinci-003", - "matchPattern": "(?i)^(text-davinci-003)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 0.00002 - } - } - ] - }, - { - "modelName": "text-embedding-ada-002-v2", - "matchPattern": "(?i)^(text-embedding-ada-002-v2)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 1e-7 - } - } - ] - }, - { - "modelName": "text-embedding-ada-002", - "matchPattern": "(?i)^(text-embedding-ada-002)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 1e-7 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-16k-0613", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k-0613)$", - "startDate": "2024-02-03T17:29:57.350Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "output": 0.000004 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-0301", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0301)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "output": 0.000002 - } - } - ] - }, - { - "modelName": "gpt-4-32k-0314", - "matchPattern": "(?i)^(openai/)?(gpt-4-32k-0314)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00006, - "output": 0.00012 - } - } - ] - }, - { - "modelName": "gpt-4-0314", - "matchPattern": "(?i)^(openai/)?(gpt-4-0314)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00003, - "output": 0.00006 - } - } - ] - }, - { - "modelName": "gpt-4", - "matchPattern": "(?i)^(openai/)?(gpt-4)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00003, - "output": 0.00006 - } - } - ] - }, - { - "modelName": "claude-instant-1.2", - "matchPattern": "(?i)^(anthropic/)?(claude-instant-1.2)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000163, - "output": 0.00000551 - } - } - ] - }, - { - "modelName": "claude-2.0", - "matchPattern": "(?i)^(anthropic/)?(claude-2.0)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000008, - "output": 0.000024 - } - } - ] - }, - { - "modelName": "claude-2.1", - "matchPattern": "(?i)^(anthropic/)?(claude-2.1)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000008, - "output": 0.000024 - } - } - ] - }, - { - "modelName": "claude-1.3", - "matchPattern": "(?i)^(anthropic/)?(claude-1.3)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000008, - "output": 0.000024 - } - } - ] - }, - { - "modelName": "claude-1.2", - "matchPattern": "(?i)^(anthropic/)?(claude-1.2)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000008, - "output": 0.000024 - } - } - ] - }, - { - "modelName": "claude-1.1", - "matchPattern": "(?i)^(anthropic/)?(claude-1.1)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000008, - "output": 0.000024 - } - } - ] - }, - { - "modelName": "claude-instant-1", - "matchPattern": "(?i)^(anthropic/)?(claude-instant-1)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000163, - "output": 0.00000551 - } - } - ] - }, - { - "modelName": "babbage-002", - "matchPattern": "(?i)^(babbage-002)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 4e-7, - "output": 0.0000016 - } - } - ] - }, - { - "modelName": "davinci-002", - "matchPattern": "(?i)^(davinci-002)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000006, - "output": 0.000012 - } - } - ] - }, - { - "modelName": "text-embedding-3-small", - "matchPattern": "(?i)^(text-embedding-3-small)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 2e-8 - } - } - ] - }, - { - "modelName": "text-embedding-3-large", - "matchPattern": "(?i)^(text-embedding-3-large)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 1.3e-7 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-0125", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0125)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-7, - "output": 0.0000015 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo)$", - "startDate": "2024-02-13T12:00:37.424Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-7, - "output": 0.0000015 - } - } - ] - }, - { - "modelName": "gpt-4-0125-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4-0125-preview)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "ft:gpt-3.5-turbo-1106", - "matchPattern": "(?i)^(ft:)(gpt-3.5-turbo-1106:)(.+)(:)(.*)(:)(.+)$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "output": 0.000006 - } - } - ] - }, - { - "modelName": "ft:gpt-3.5-turbo-0613", - "matchPattern": "(?i)^(ft:)(gpt-3.5-turbo-0613:)(.+)(:)(.*)(:)(.+)$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000012, - "output": 0.000016 - } - } - ] - }, - { - "modelName": "ft:davinci-002", - "matchPattern": "(?i)^(ft:)(davinci-002:)(.+)(:)(.*)(:)(.+)$$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000012, - "output": 0.000012 - } - } - ] - }, - { - "modelName": "ft:babbage-002", - "matchPattern": "(?i)^(ft:)(babbage-002:)(.+)(:)(.*)(:)(.+)$$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000016, - "output": 0.0000016 - } - } - ] - }, - { - "modelName": "chat-bison", - "matchPattern": "(?i)^(chat-bison)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "codechat-bison-32k", - "matchPattern": "(?i)^(codechat-bison-32k)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "codechat-bison", - "matchPattern": "(?i)^(codechat-bison)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "text-bison-32k", - "matchPattern": "(?i)^(text-bison-32k)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "chat-bison-32k", - "matchPattern": "(?i)^(chat-bison-32k)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "text-unicorn", - "matchPattern": "(?i)^(text-unicorn)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "output": 0.0000075 - } - } - ] - }, - { - "modelName": "text-bison", - "matchPattern": "(?i)^(text-bison)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "textembedding-gecko", - "matchPattern": "(?i)^(textembedding-gecko)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 1e-7 - } - } - ] - }, - { - "modelName": "textembedding-gecko-multilingual", - "matchPattern": "(?i)^(textembedding-gecko-multilingual)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 1e-7 - } - } - ] - }, - { - "modelName": "code-gecko", - "matchPattern": "(?i)^(code-gecko)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "code-bison", - "matchPattern": "(?i)^(code-bison)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "code-bison-32k", - "matchPattern": "(?i)^(code-bison-32k)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-16k", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k)$", - "startDate": "2024-02-13T12:00:37.424Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-7, - "output": 0.0000015 - } - } - ] - }, - { - "modelName": "gpt-4-turbo-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4-turbo-preview)$", - "startDate": "2024-02-15T21:21:50.947Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "claude-3-opus-20240229", - "matchPattern": "(?i)^(anthropic/)?(claude-3-opus-20240229|anthropic\\.claude-3-opus-20240229-v1:0|claude-3-opus@20240229)$", - "startDate": "2024-03-07T17:55:38.139Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "output": 0.000075 - } - } - ] - }, - { - "modelName": "claude-3-sonnet-20240229", - "matchPattern": "(?i)^(anthropic/)?(claude-3-sonnet-20240229|anthropic\\.claude-3-sonnet-20240229-v1:0|claude-3-sonnet@20240229)$", - "startDate": "2024-03-07T17:55:38.139Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-3-haiku-20240307", - "matchPattern": "(?i)^(anthropic/)?(claude-3-haiku-20240307|anthropic\\.claude-3-haiku-20240307-v1:0|claude-3-haiku@20240307)$", - "startDate": "2024-03-14T09:41:18.736Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 0.00000125 - } - } - ] - }, - { - "modelName": "gemini-1.0-pro-latest", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.0-pro-latest)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-04-11T10:27:46.517Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "gemini-1.0-pro", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.0-pro)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-04-11T10:27:46.517Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1.25e-7, - "output": 3.75e-7 - } - } - ] - }, - { - "modelName": "gemini-1.0-pro-001", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.0-pro-001)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-04-11T10:27:46.517Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1.25e-7, - "output": 3.75e-7 - } - } - ] - }, - { - "modelName": "gemini-pro", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-pro)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-04-11T10:27:46.517Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1.25e-7, - "output": 3.75e-7 - } - } - ] - }, - { - "modelName": "gemini-1.5-pro-latest", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.5-pro-latest)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-04-11T10:27:46.517Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "output": 0.0000075 - } - } - ] - }, - { - "modelName": "gpt-4-turbo-2024-04-09", - "matchPattern": "(?i)^(openai/)?(gpt-4-turbo-2024-04-09)$", - "startDate": "2024-04-23T10:37:17.092Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "gpt-4-turbo", - "matchPattern": "(?i)^(openai/)?(gpt-4-turbo)$", - "startDate": "2024-04-11T21:13:44.989Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "gpt-4-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4-preview)$", - "startDate": "2024-04-23T10:37:17.092Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "claude-3-5-sonnet-20240620", - "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-20240620|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20240620-v1:0|claude-3-5-sonnet@20240620)$", - "startDate": "2024-06-25T11:47:24.475Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "gpt-4o-mini", - "matchPattern": "(?i)^(openai/)?(gpt-4o-mini)$", - "startDate": "2024-07-18T17:56:09.591Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1.5e-7, - "output": 6e-7, - "input_cached_tokens": 7.5e-8, - "input_cache_read": 7.5e-8 - } - } - ] - }, - { - "modelName": "gpt-4o-mini-2024-07-18", - "matchPattern": "(?i)^(openai/)?(gpt-4o-mini-2024-07-18)$", - "startDate": "2024-07-18T17:56:09.591Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1.5e-7, - "input_cached_tokens": 7.5e-8, - "input_cache_read": 7.5e-8, - "output": 6e-7 - } - } - ] - }, - { - "modelName": "gpt-4o-2024-08-06", - "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-08-06)$", - "startDate": "2024-08-07T11:54:31.298Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "input_cached_tokens": 0.00000125, - "input_cache_read": 0.00000125, - "output": 0.00001 - } - } - ] - }, - { - "modelName": "o1-preview", - "matchPattern": "(?i)^(openai/)?(o1-preview)$", - "startDate": "2024-09-13T10:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_cached_tokens": 0.0000075, - "input_cache_read": 0.0000075, - "output": 0.00006, - "output_reasoning_tokens": 0.00006, - "output_reasoning": 0.00006 - } - } - ] - }, - { - "modelName": "o1-preview-2024-09-12", - "matchPattern": "(?i)^(openai/)?(o1-preview-2024-09-12)$", - "startDate": "2024-09-13T10:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_cached_tokens": 0.0000075, - "input_cache_read": 0.0000075, - "output": 0.00006, - "output_reasoning_tokens": 0.00006, - "output_reasoning": 0.00006 - } - } - ] - }, - { - "modelName": "o1-mini", - "matchPattern": "(?i)^(openai/)?(o1-mini)$", - "startDate": "2024-09-13T10:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 5.5e-7, - "input_cache_read": 5.5e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "o1-mini-2024-09-12", - "matchPattern": "(?i)^(openai/)?(o1-mini-2024-09-12)$", - "startDate": "2024-09-13T10:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 5.5e-7, - "input_cache_read": 5.5e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "claude-3.5-sonnet-20241022", - "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20241022-v2:0|claude-3-5-sonnet-V2@20241022)$", - "startDate": "2024-10-22T18:48:01.676Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-3.5-sonnet-latest", - "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-latest)$", - "startDate": "2024-10-22T18:48:01.676Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-3-5-haiku-20241022", - "matchPattern": "(?i)^(anthropic/)?(claude-3-5-haiku-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-haiku-20241022-v1:0|claude-3-5-haiku-V1@20241022)$", - "startDate": "2024-11-05T10:30:50.566Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 8e-7, - "input_tokens": 8e-7, - "output": 0.000004, - "output_tokens": 0.000004, - "cache_creation_input_tokens": 0.000001, - "input_cache_creation": 0.000001, - "input_cache_creation_5m": 0.000001, - "input_cache_creation_1h": 0.0000016, - "cache_read_input_tokens": 8e-8, - "input_cache_read": 8e-8 - } - } - ] - }, - { - "modelName": "claude-3.5-haiku-latest", - "matchPattern": "(?i)^(anthropic/)?(claude-3-5-haiku-latest)$", - "startDate": "2024-11-05T10:30:50.566Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 8e-7, - "input_tokens": 8e-7, - "output": 0.000004, - "output_tokens": 0.000004, - "cache_creation_input_tokens": 0.000001, - "input_cache_creation": 0.000001, - "input_cache_creation_5m": 0.000001, - "input_cache_creation_1h": 0.0000016, - "cache_read_input_tokens": 8e-8, - "input_cache_read": 8e-8 - } - } - ] - }, - { - "modelName": "chatgpt-4o-latest", - "matchPattern": "(?i)^(chatgpt-4o-latest)$", - "startDate": "2024-11-25T12:47:17.504Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000005, - "output": 0.000015 - } - } - ] - }, - { - "modelName": "gpt-4o-2024-11-20", - "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-11-20)$", - "startDate": "2024-12-03T10:06:12.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "input_cached_tokens": 0.00000125, - "input_cache_read": 0.00000125, - "output": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-4o-audio-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4o-audio-preview)$", - "startDate": "2024-12-03T10:19:56.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input_text_tokens": 0.0000025, - "output_text_tokens": 0.00001, - "input_audio_tokens": 0.0001, - "input_audio": 0.0001, - "output_audio_tokens": 0.0002, - "output_audio": 0.0002 - } - } - ] - }, - { - "modelName": "gpt-4o-audio-preview-2024-10-01", - "matchPattern": "(?i)^(openai/)?(gpt-4o-audio-preview-2024-10-01)$", - "startDate": "2024-12-03T10:19:56.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input_text_tokens": 0.0000025, - "output_text_tokens": 0.00001, - "input_audio_tokens": 0.0001, - "input_audio": 0.0001, - "output_audio_tokens": 0.0002, - "output_audio": 0.0002 - } - } - ] - }, - { - "modelName": "gpt-4o-realtime-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4o-realtime-preview)$", - "startDate": "2024-12-03T10:19:56.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input_text_tokens": 0.000005, - "input_cached_text_tokens": 0.0000025, - "output_text_tokens": 0.00002, - "input_audio_tokens": 0.0001, - "input_audio": 0.0001, - "input_cached_audio_tokens": 0.00002, - "output_audio_tokens": 0.0002, - "output_audio": 0.0002 - } - } - ] - }, - { - "modelName": "gpt-4o-realtime-preview-2024-10-01", - "matchPattern": "(?i)^(openai/)?(gpt-4o-realtime-preview-2024-10-01)$", - "startDate": "2024-12-03T10:19:56.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input_text_tokens": 0.000005, - "input_cached_text_tokens": 0.0000025, - "output_text_tokens": 0.00002, - "input_audio_tokens": 0.0001, - "input_audio": 0.0001, - "input_cached_audio_tokens": 0.00002, - "output_audio_tokens": 0.0002, - "output_audio": 0.0002 - } - } - ] - }, - { - "modelName": "o1", - "matchPattern": "(?i)^(openai/)?(o1)$", - "startDate": "2025-01-17T00:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_cached_tokens": 0.0000075, - "input_cache_read": 0.0000075, - "output": 0.00006, - "output_reasoning_tokens": 0.00006, - "output_reasoning": 0.00006 - } - } - ] - }, - { - "modelName": "o1-2024-12-17", - "matchPattern": "(?i)^(openai/)?(o1-2024-12-17)$", - "startDate": "2025-01-17T00:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_cached_tokens": 0.0000075, - "input_cache_read": 0.0000075, - "output": 0.00006, - "output_reasoning_tokens": 0.00006, - "output_reasoning": 0.00006 - } - } - ] - }, - { - "modelName": "o3-mini", - "matchPattern": "(?i)^(openai/)?(o3-mini)$", - "startDate": "2025-01-31T20:41:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 5.5e-7, - "input_cache_read": 5.5e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "o3-mini-2025-01-31", - "matchPattern": "(?i)^(openai/)?(o3-mini-2025-01-31)$", - "startDate": "2025-01-31T20:41:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 5.5e-7, - "input_cache_read": 5.5e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "gemini-2.0-flash-001", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash-001)(@[a-zA-Z0-9]+)?$", - "startDate": "2025-02-06T11:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1e-7, - "output": 4e-7 - } - } - ] - }, - { - "modelName": "gemini-2.0-flash-lite-preview-02-05", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview-02-05)(@[a-zA-Z0-9]+)?$", - "startDate": "2025-02-06T11:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 7.5e-8, - "output": 3e-7 - } - } - ] - }, - { - "modelName": "claude-3.7-sonnet-20250219", - "matchPattern": "(?i)^(anthropic/)?(claude-3.7-sonnet-20250219|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3.7-sonnet-20250219-v1:0|claude-3-7-sonnet-V1@20250219)$", - "startDate": "2025-02-25T09:35:39.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-3.7-sonnet-latest", - "matchPattern": "(?i)^(anthropic/)?(claude-3-7-sonnet-latest)$", - "startDate": "2025-02-25T09:35:39.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "gpt-4.5-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4.5-preview)$", - "startDate": "2025-02-27T21:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000075, - "input_cached_tokens": 0.0000375, - "input_cached_text_tokens": 0.0000375, - "input_cache_read": 0.0000375, - "output": 0.00015 - } - } - ] - }, - { - "modelName": "gpt-4.5-preview-2025-02-27", - "matchPattern": "(?i)^(openai/)?(gpt-4.5-preview-2025-02-27)$", - "startDate": "2025-02-27T21:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000075, - "input_cached_tokens": 0.0000375, - "input_cached_text_tokens": 0.0000375, - "input_cache_read": 0.0000375, - "output": 0.00015 - } - } - ] - }, - { - "modelName": "gpt-4.1", - "matchPattern": "(?i)^(openai/)?(gpt-4.1)$", - "startDate": "2025-04-15T10:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_cached_tokens": 5e-7, - "input_cached_text_tokens": 5e-7, - "input_cache_read": 5e-7, - "output": 0.000008 - } - } - ] - }, - { - "modelName": "gpt-4.1-2025-04-14", - "matchPattern": "(?i)^(openai/)?(gpt-4.1-2025-04-14)$", - "startDate": "2025-04-15T10:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_cached_tokens": 5e-7, - "input_cached_text_tokens": 5e-7, - "input_cache_read": 5e-7, - "output": 0.000008 - } - } - ] - }, - { - "modelName": "gpt-4.1-mini-2025-04-14", - "matchPattern": "(?i)^(openai/)?(gpt-4.1-mini-2025-04-14)$", - "startDate": "2025-04-15T10:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 4e-7, - "input_cached_tokens": 1e-7, - "input_cached_text_tokens": 1e-7, - "input_cache_read": 1e-7, - "output": 0.0000016 - } - } - ] - }, - { - "modelName": "gpt-4.1-nano-2025-04-14", - "matchPattern": "(?i)^(openai/)?(gpt-4.1-nano-2025-04-14)$", - "startDate": "2025-04-15T10:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1e-7, - "input_cached_tokens": 2.5e-8, - "input_cached_text_tokens": 2.5e-8, - "input_cache_read": 2.5e-8, - "output": 4e-7 - } - } - ] - }, - { - "modelName": "o3", - "matchPattern": "(?i)^(openai/)?(o3)$", - "startDate": "2025-04-16T23:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_cached_tokens": 5e-7, - "input_cache_read": 5e-7, - "output": 0.000008, - "output_reasoning_tokens": 0.000008, - "output_reasoning": 0.000008 - } - } - ] - }, - { - "modelName": "o3-2025-04-16", - "matchPattern": "(?i)^(openai/)?(o3-2025-04-16)$", - "startDate": "2025-04-16T23:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_cached_tokens": 5e-7, - "input_cache_read": 5e-7, - "output": 0.000008, - "output_reasoning_tokens": 0.000008, - "output_reasoning": 0.000008 - } - } - ] - }, - { - "modelName": "o4-mini", - "matchPattern": "(?i)^(o4-mini)$", - "startDate": "2025-04-16T23:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 2.75e-7, - "input_cache_read": 2.75e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "o4-mini-2025-04-16", - "matchPattern": "(?i)^(o4-mini-2025-04-16)$", - "startDate": "2025-04-16T23:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 2.75e-7, - "input_cache_read": 2.75e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "gemini-2.0-flash", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash)(@[a-zA-Z0-9]+)?$", - "startDate": "2025-04-22T10:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1e-7, - "output": 4e-7 - } - } - ] - }, - { - "modelName": "gemini-2.0-flash-lite-preview", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview)(@[a-zA-Z0-9]+)?$", - "startDate": "2025-04-22T10:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 7.5e-8, - "output": 3e-7 - } - } - ] - }, - { - "modelName": "gpt-4.1-nano", - "matchPattern": "(?i)^(openai/)?(gpt-4.1-nano)$", - "startDate": "2025-04-22T10:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1e-7, - "input_cached_tokens": 2.5e-8, - "input_cached_text_tokens": 2.5e-8, - "input_cache_read": 2.5e-8, - "output": 4e-7 - } - } - ] - }, - { - "modelName": "gpt-4.1-mini", - "matchPattern": "(?i)^(openai/)?(gpt-4.1-mini)$", - "startDate": "2025-04-22T10:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 4e-7, - "input_cached_tokens": 1e-7, - "input_cached_text_tokens": 1e-7, - "input_cache_read": 1e-7, - "output": 0.0000016 - } - } - ] - }, - { - "modelName": "claude-sonnet-4-5-20250929", - "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4-5(-20250929)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-5(-20250929)?-v1(:0)?|claude-sonnet-4-5-V1(@20250929)?|claude-sonnet-4-5(@20250929)?)$", - "startDate": "2025-09-29T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - }, - { - "name": "Large Context", - "isDefault": false, - "priority": 1, - "conditions": [ + modelName: "gpt-4o", + matchPattern: "(?i)^(openai/)?(gpt-4o)$", + startDate: "2024-05-13T23:15:07.670Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + input_cached_tokens: 0.00000125, + input_cache_read: 0.00000125, + output: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-4o-2024-05-13", + matchPattern: "(?i)^(openai/)?(gpt-4o-2024-05-13)$", + startDate: "2024-05-13T23:15:07.670Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000005, + output: 0.000015, + }, + }, + ], + }, + { + modelName: "gpt-4-1106-preview", + matchPattern: "(?i)^(openai/)?(gpt-4-1106-preview)$", + startDate: "2024-04-23T10:37:17.092Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "gpt-4-turbo-vision", + matchPattern: "(?i)^(openai/)?(gpt-4(-\\d{4})?-vision-preview)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "gpt-4-32k", + matchPattern: "(?i)^(openai/)?(gpt-4-32k)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00006, + output: 0.00012, + }, + }, + ], + }, + { + modelName: "gpt-4-32k-0613", + matchPattern: "(?i)^(openai/)?(gpt-4-32k-0613)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00006, + output: 0.00012, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-1106", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-1106)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000001, + output: 0.000002, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-0613", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0613)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000015, + output: 0.000002, + }, + }, + ], + }, + { + modelName: "gpt-4-0613", + matchPattern: "(?i)^(openai/)?(gpt-4-0613)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00003, + output: 0.00006, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-instruct", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-instruct)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000015, + output: 0.000002, + }, + }, + ], + }, + { + modelName: "text-ada-001", + matchPattern: "(?i)^(text-ada-001)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 0.000004, + }, + }, + ], + }, + { + modelName: "text-babbage-001", + matchPattern: "(?i)^(text-babbage-001)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 5e-7, + }, + }, + ], + }, + { + modelName: "text-curie-001", + matchPattern: "(?i)^(text-curie-001)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 0.00002, + }, + }, + ], + }, + { + modelName: "text-davinci-001", + matchPattern: "(?i)^(text-davinci-001)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 0.00002, + }, + }, + ], + }, + { + modelName: "text-davinci-002", + matchPattern: "(?i)^(text-davinci-002)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 0.00002, + }, + }, + ], + }, + { + modelName: "text-davinci-003", + matchPattern: "(?i)^(text-davinci-003)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 0.00002, + }, + }, + ], + }, + { + modelName: "text-embedding-ada-002-v2", + matchPattern: "(?i)^(text-embedding-ada-002-v2)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 1e-7, + }, + }, + ], + }, + { + modelName: "text-embedding-ada-002", + matchPattern: "(?i)^(text-embedding-ada-002)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 1e-7, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-16k-0613", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k-0613)$", + startDate: "2024-02-03T17:29:57.350Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + output: 0.000004, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-0301", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0301)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + output: 0.000002, + }, + }, + ], + }, + { + modelName: "gpt-4-32k-0314", + matchPattern: "(?i)^(openai/)?(gpt-4-32k-0314)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00006, + output: 0.00012, + }, + }, + ], + }, + { + modelName: "gpt-4-0314", + matchPattern: "(?i)^(openai/)?(gpt-4-0314)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00003, + output: 0.00006, + }, + }, + ], + }, + { + modelName: "gpt-4", + matchPattern: "(?i)^(openai/)?(gpt-4)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00003, + output: 0.00006, + }, + }, + ], + }, + { + modelName: "claude-instant-1.2", + matchPattern: "(?i)^(anthropic/)?(claude-instant-1.2)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000163, + output: 0.00000551, + }, + }, + ], + }, + { + modelName: "claude-2.0", + matchPattern: "(?i)^(anthropic/)?(claude-2.0)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000008, + output: 0.000024, + }, + }, + ], + }, + { + modelName: "claude-2.1", + matchPattern: "(?i)^(anthropic/)?(claude-2.1)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000008, + output: 0.000024, + }, + }, + ], + }, + { + modelName: "claude-1.3", + matchPattern: "(?i)^(anthropic/)?(claude-1.3)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000008, + output: 0.000024, + }, + }, + ], + }, + { + modelName: "claude-1.2", + matchPattern: "(?i)^(anthropic/)?(claude-1.2)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000008, + output: 0.000024, + }, + }, + ], + }, + { + modelName: "claude-1.1", + matchPattern: "(?i)^(anthropic/)?(claude-1.1)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000008, + output: 0.000024, + }, + }, + ], + }, + { + modelName: "claude-instant-1", + matchPattern: "(?i)^(anthropic/)?(claude-instant-1)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000163, + output: 0.00000551, + }, + }, + ], + }, + { + modelName: "babbage-002", + matchPattern: "(?i)^(babbage-002)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 4e-7, + output: 0.0000016, + }, + }, + ], + }, + { + modelName: "davinci-002", + matchPattern: "(?i)^(davinci-002)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000006, + output: 0.000012, + }, + }, + ], + }, + { + modelName: "text-embedding-3-small", + matchPattern: "(?i)^(text-embedding-3-small)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 2e-8, + }, + }, + ], + }, + { + modelName: "text-embedding-3-large", + matchPattern: "(?i)^(text-embedding-3-large)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 1.3e-7, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-0125", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0125)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-7, + output: 0.0000015, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo)$", + startDate: "2024-02-13T12:00:37.424Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-7, + output: 0.0000015, + }, + }, + ], + }, + { + modelName: "gpt-4-0125-preview", + matchPattern: "(?i)^(openai/)?(gpt-4-0125-preview)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "ft:gpt-3.5-turbo-1106", + matchPattern: "(?i)^(ft:)(gpt-3.5-turbo-1106:)(.+)(:)(.*)(:)(.+)$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + output: 0.000006, + }, + }, + ], + }, + { + modelName: "ft:gpt-3.5-turbo-0613", + matchPattern: "(?i)^(ft:)(gpt-3.5-turbo-0613:)(.+)(:)(.*)(:)(.+)$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000012, + output: 0.000016, + }, + }, + ], + }, + { + modelName: "ft:davinci-002", + matchPattern: "(?i)^(ft:)(davinci-002:)(.+)(:)(.*)(:)(.+)$$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000012, + output: 0.000012, + }, + }, + ], + }, + { + modelName: "ft:babbage-002", + matchPattern: "(?i)^(ft:)(babbage-002:)(.+)(:)(.*)(:)(.+)$$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000016, + output: 0.0000016, + }, + }, + ], + }, + { + modelName: "chat-bison", + matchPattern: "(?i)^(chat-bison)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "codechat-bison-32k", + matchPattern: "(?i)^(codechat-bison-32k)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "codechat-bison", + matchPattern: "(?i)^(codechat-bison)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "text-bison-32k", + matchPattern: "(?i)^(text-bison-32k)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "chat-bison-32k", + matchPattern: "(?i)^(chat-bison-32k)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "text-unicorn", + matchPattern: "(?i)^(text-unicorn)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + output: 0.0000075, + }, + }, + ], + }, + { + modelName: "text-bison", + matchPattern: "(?i)^(text-bison)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "textembedding-gecko", + matchPattern: "(?i)^(textembedding-gecko)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 1e-7, + }, + }, + ], + }, + { + modelName: "textembedding-gecko-multilingual", + matchPattern: "(?i)^(textembedding-gecko-multilingual)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 1e-7, + }, + }, + ], + }, + { + modelName: "code-gecko", + matchPattern: "(?i)^(code-gecko)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "code-bison", + matchPattern: "(?i)^(code-bison)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "code-bison-32k", + matchPattern: "(?i)^(code-bison-32k)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-16k", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k)$", + startDate: "2024-02-13T12:00:37.424Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-7, + output: 0.0000015, + }, + }, + ], + }, + { + modelName: "gpt-4-turbo-preview", + matchPattern: "(?i)^(openai/)?(gpt-4-turbo-preview)$", + startDate: "2024-02-15T21:21:50.947Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "claude-3-opus-20240229", + matchPattern: + "(?i)^(anthropic/)?(claude-3-opus-20240229|anthropic\\.claude-3-opus-20240229-v1:0|claude-3-opus@20240229)$", + startDate: "2024-03-07T17:55:38.139Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + output: 0.000075, + }, + }, + ], + }, + { + modelName: "claude-3-sonnet-20240229", + matchPattern: + "(?i)^(anthropic/)?(claude-3-sonnet-20240229|anthropic\\.claude-3-sonnet-20240229-v1:0|claude-3-sonnet@20240229)$", + startDate: "2024-03-07T17:55:38.139Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-3-haiku-20240307", + matchPattern: + "(?i)^(anthropic/)?(claude-3-haiku-20240307|anthropic\\.claude-3-haiku-20240307-v1:0|claude-3-haiku@20240307)$", + startDate: "2024-03-14T09:41:18.736Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 0.00000125, + }, + }, + ], + }, + { + modelName: "gemini-1.0-pro-latest", + matchPattern: "(?i)^(google(ai)?/)?(gemini-1.0-pro-latest)(@[a-zA-Z0-9]+)?$", + startDate: "2024-04-11T10:27:46.517Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "gemini-1.0-pro", + matchPattern: "(?i)^(google(ai)?/)?(gemini-1.0-pro)(@[a-zA-Z0-9]+)?$", + startDate: "2024-04-11T10:27:46.517Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1.25e-7, + output: 3.75e-7, + }, + }, + ], + }, + { + modelName: "gemini-1.0-pro-001", + matchPattern: "(?i)^(google(ai)?/)?(gemini-1.0-pro-001)(@[a-zA-Z0-9]+)?$", + startDate: "2024-04-11T10:27:46.517Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1.25e-7, + output: 3.75e-7, + }, + }, + ], + }, + { + modelName: "gemini-pro", + matchPattern: "(?i)^(google(ai)?/)?(gemini-pro)(@[a-zA-Z0-9]+)?$", + startDate: "2024-04-11T10:27:46.517Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1.25e-7, + output: 3.75e-7, + }, + }, + ], + }, + { + modelName: "gemini-1.5-pro-latest", + matchPattern: "(?i)^(google(ai)?/)?(gemini-1.5-pro-latest)(@[a-zA-Z0-9]+)?$", + startDate: "2024-04-11T10:27:46.517Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + output: 0.0000075, + }, + }, + ], + }, + { + modelName: "gpt-4-turbo-2024-04-09", + matchPattern: "(?i)^(openai/)?(gpt-4-turbo-2024-04-09)$", + startDate: "2024-04-23T10:37:17.092Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "gpt-4-turbo", + matchPattern: "(?i)^(openai/)?(gpt-4-turbo)$", + startDate: "2024-04-11T21:13:44.989Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "gpt-4-preview", + matchPattern: "(?i)^(openai/)?(gpt-4-preview)$", + startDate: "2024-04-23T10:37:17.092Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "claude-3-5-sonnet-20240620", + matchPattern: + "(?i)^(anthropic/)?(claude-3-5-sonnet-20240620|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20240620-v1:0|claude-3-5-sonnet@20240620)$", + startDate: "2024-06-25T11:47:24.475Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "gpt-4o-mini", + matchPattern: "(?i)^(openai/)?(gpt-4o-mini)$", + startDate: "2024-07-18T17:56:09.591Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1.5e-7, + output: 6e-7, + input_cached_tokens: 7.5e-8, + input_cache_read: 7.5e-8, + }, + }, + ], + }, + { + modelName: "gpt-4o-mini-2024-07-18", + matchPattern: "(?i)^(openai/)?(gpt-4o-mini-2024-07-18)$", + startDate: "2024-07-18T17:56:09.591Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1.5e-7, + input_cached_tokens: 7.5e-8, + input_cache_read: 7.5e-8, + output: 6e-7, + }, + }, + ], + }, + { + modelName: "gpt-4o-2024-08-06", + matchPattern: "(?i)^(openai/)?(gpt-4o-2024-08-06)$", + startDate: "2024-08-07T11:54:31.298Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + input_cached_tokens: 0.00000125, + input_cache_read: 0.00000125, + output: 0.00001, + }, + }, + ], + }, + { + modelName: "o1-preview", + matchPattern: "(?i)^(openai/)?(o1-preview)$", + startDate: "2024-09-13T10:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_cached_tokens: 0.0000075, + input_cache_read: 0.0000075, + output: 0.00006, + output_reasoning_tokens: 0.00006, + output_reasoning: 0.00006, + }, + }, + ], + }, + { + modelName: "o1-preview-2024-09-12", + matchPattern: "(?i)^(openai/)?(o1-preview-2024-09-12)$", + startDate: "2024-09-13T10:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_cached_tokens: 0.0000075, + input_cache_read: 0.0000075, + output: 0.00006, + output_reasoning_tokens: 0.00006, + output_reasoning: 0.00006, + }, + }, + ], + }, + { + modelName: "o1-mini", + matchPattern: "(?i)^(openai/)?(o1-mini)$", + startDate: "2024-09-13T10:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 5.5e-7, + input_cache_read: 5.5e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "o1-mini-2024-09-12", + matchPattern: "(?i)^(openai/)?(o1-mini-2024-09-12)$", + startDate: "2024-09-13T10:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 5.5e-7, + input_cache_read: 5.5e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "claude-3.5-sonnet-20241022", + matchPattern: + "(?i)^(anthropic/)?(claude-3-5-sonnet-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20241022-v2:0|claude-3-5-sonnet-V2@20241022)$", + startDate: "2024-10-22T18:48:01.676Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-3.5-sonnet-latest", + matchPattern: "(?i)^(anthropic/)?(claude-3-5-sonnet-latest)$", + startDate: "2024-10-22T18:48:01.676Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-3-5-haiku-20241022", + matchPattern: + "(?i)^(anthropic/)?(claude-3-5-haiku-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-haiku-20241022-v1:0|claude-3-5-haiku-V1@20241022)$", + startDate: "2024-11-05T10:30:50.566Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 8e-7, + input_tokens: 8e-7, + output: 0.000004, + output_tokens: 0.000004, + cache_creation_input_tokens: 0.000001, + input_cache_creation: 0.000001, + input_cache_creation_5m: 0.000001, + input_cache_creation_1h: 0.0000016, + cache_read_input_tokens: 8e-8, + input_cache_read: 8e-8, + }, + }, + ], + }, + { + modelName: "claude-3.5-haiku-latest", + matchPattern: "(?i)^(anthropic/)?(claude-3-5-haiku-latest)$", + startDate: "2024-11-05T10:30:50.566Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 8e-7, + input_tokens: 8e-7, + output: 0.000004, + output_tokens: 0.000004, + cache_creation_input_tokens: 0.000001, + input_cache_creation: 0.000001, + input_cache_creation_5m: 0.000001, + input_cache_creation_1h: 0.0000016, + cache_read_input_tokens: 8e-8, + input_cache_read: 8e-8, + }, + }, + ], + }, + { + modelName: "chatgpt-4o-latest", + matchPattern: "(?i)^(chatgpt-4o-latest)$", + startDate: "2024-11-25T12:47:17.504Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000005, + output: 0.000015, + }, + }, + ], + }, + { + modelName: "gpt-4o-2024-11-20", + matchPattern: "(?i)^(openai/)?(gpt-4o-2024-11-20)$", + startDate: "2024-12-03T10:06:12.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + input_cached_tokens: 0.00000125, + input_cache_read: 0.00000125, + output: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-4o-audio-preview", + matchPattern: "(?i)^(openai/)?(gpt-4o-audio-preview)$", + startDate: "2024-12-03T10:19:56.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input_text_tokens: 0.0000025, + output_text_tokens: 0.00001, + input_audio_tokens: 0.0001, + input_audio: 0.0001, + output_audio_tokens: 0.0002, + output_audio: 0.0002, + }, + }, + ], + }, + { + modelName: "gpt-4o-audio-preview-2024-10-01", + matchPattern: "(?i)^(openai/)?(gpt-4o-audio-preview-2024-10-01)$", + startDate: "2024-12-03T10:19:56.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input_text_tokens: 0.0000025, + output_text_tokens: 0.00001, + input_audio_tokens: 0.0001, + input_audio: 0.0001, + output_audio_tokens: 0.0002, + output_audio: 0.0002, + }, + }, + ], + }, + { + modelName: "gpt-4o-realtime-preview", + matchPattern: "(?i)^(openai/)?(gpt-4o-realtime-preview)$", + startDate: "2024-12-03T10:19:56.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input_text_tokens: 0.000005, + input_cached_text_tokens: 0.0000025, + output_text_tokens: 0.00002, + input_audio_tokens: 0.0001, + input_audio: 0.0001, + input_cached_audio_tokens: 0.00002, + output_audio_tokens: 0.0002, + output_audio: 0.0002, + }, + }, + ], + }, + { + modelName: "gpt-4o-realtime-preview-2024-10-01", + matchPattern: "(?i)^(openai/)?(gpt-4o-realtime-preview-2024-10-01)$", + startDate: "2024-12-03T10:19:56.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input_text_tokens: 0.000005, + input_cached_text_tokens: 0.0000025, + output_text_tokens: 0.00002, + input_audio_tokens: 0.0001, + input_audio: 0.0001, + input_cached_audio_tokens: 0.00002, + output_audio_tokens: 0.0002, + output_audio: 0.0002, + }, + }, + ], + }, + { + modelName: "o1", + matchPattern: "(?i)^(openai/)?(o1)$", + startDate: "2025-01-17T00:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_cached_tokens: 0.0000075, + input_cache_read: 0.0000075, + output: 0.00006, + output_reasoning_tokens: 0.00006, + output_reasoning: 0.00006, + }, + }, + ], + }, + { + modelName: "o1-2024-12-17", + matchPattern: "(?i)^(openai/)?(o1-2024-12-17)$", + startDate: "2025-01-17T00:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_cached_tokens: 0.0000075, + input_cache_read: 0.0000075, + output: 0.00006, + output_reasoning_tokens: 0.00006, + output_reasoning: 0.00006, + }, + }, + ], + }, + { + modelName: "o3-mini", + matchPattern: "(?i)^(openai/)?(o3-mini)$", + startDate: "2025-01-31T20:41:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 5.5e-7, + input_cache_read: 5.5e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "o3-mini-2025-01-31", + matchPattern: "(?i)^(openai/)?(o3-mini-2025-01-31)$", + startDate: "2025-01-31T20:41:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 5.5e-7, + input_cache_read: 5.5e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "gemini-2.0-flash-001", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash-001)(@[a-zA-Z0-9]+)?$", + startDate: "2025-02-06T11:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1e-7, + output: 4e-7, + }, + }, + ], + }, + { + modelName: "gemini-2.0-flash-lite-preview-02-05", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview-02-05)(@[a-zA-Z0-9]+)?$", + startDate: "2025-02-06T11:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 7.5e-8, + output: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-3.7-sonnet-20250219", + matchPattern: + "(?i)^(anthropic/)?(claude-3.7-sonnet-20250219|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3.7-sonnet-20250219-v1:0|claude-3-7-sonnet-V1@20250219)$", + startDate: "2025-02-25T09:35:39.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-3.7-sonnet-latest", + matchPattern: "(?i)^(anthropic/)?(claude-3-7-sonnet-latest)$", + startDate: "2025-02-25T09:35:39.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "gpt-4.5-preview", + matchPattern: "(?i)^(openai/)?(gpt-4.5-preview)$", + startDate: "2025-02-27T21:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000075, + input_cached_tokens: 0.0000375, + input_cached_text_tokens: 0.0000375, + input_cache_read: 0.0000375, + output: 0.00015, + }, + }, + ], + }, + { + modelName: "gpt-4.5-preview-2025-02-27", + matchPattern: "(?i)^(openai/)?(gpt-4.5-preview-2025-02-27)$", + startDate: "2025-02-27T21:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000075, + input_cached_tokens: 0.0000375, + input_cached_text_tokens: 0.0000375, + input_cache_read: 0.0000375, + output: 0.00015, + }, + }, + ], + }, + { + modelName: "gpt-4.1", + matchPattern: "(?i)^(openai/)?(gpt-4.1)$", + startDate: "2025-04-15T10:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_cached_tokens: 5e-7, + input_cached_text_tokens: 5e-7, + input_cache_read: 5e-7, + output: 0.000008, + }, + }, + ], + }, + { + modelName: "gpt-4.1-2025-04-14", + matchPattern: "(?i)^(openai/)?(gpt-4.1-2025-04-14)$", + startDate: "2025-04-15T10:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_cached_tokens: 5e-7, + input_cached_text_tokens: 5e-7, + input_cache_read: 5e-7, + output: 0.000008, + }, + }, + ], + }, + { + modelName: "gpt-4.1-mini-2025-04-14", + matchPattern: "(?i)^(openai/)?(gpt-4.1-mini-2025-04-14)$", + startDate: "2025-04-15T10:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 4e-7, + input_cached_tokens: 1e-7, + input_cached_text_tokens: 1e-7, + input_cache_read: 1e-7, + output: 0.0000016, + }, + }, + ], + }, + { + modelName: "gpt-4.1-nano-2025-04-14", + matchPattern: "(?i)^(openai/)?(gpt-4.1-nano-2025-04-14)$", + startDate: "2025-04-15T10:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1e-7, + input_cached_tokens: 2.5e-8, + input_cached_text_tokens: 2.5e-8, + input_cache_read: 2.5e-8, + output: 4e-7, + }, + }, + ], + }, + { + modelName: "o3", + matchPattern: "(?i)^(openai/)?(o3)$", + startDate: "2025-04-16T23:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_cached_tokens: 5e-7, + input_cache_read: 5e-7, + output: 0.000008, + output_reasoning_tokens: 0.000008, + output_reasoning: 0.000008, + }, + }, + ], + }, + { + modelName: "o3-2025-04-16", + matchPattern: "(?i)^(openai/)?(o3-2025-04-16)$", + startDate: "2025-04-16T23:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_cached_tokens: 5e-7, + input_cache_read: 5e-7, + output: 0.000008, + output_reasoning_tokens: 0.000008, + output_reasoning: 0.000008, + }, + }, + ], + }, + { + modelName: "o4-mini", + matchPattern: "(?i)^(o4-mini)$", + startDate: "2025-04-16T23:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 2.75e-7, + input_cache_read: 2.75e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "o4-mini-2025-04-16", + matchPattern: "(?i)^(o4-mini-2025-04-16)$", + startDate: "2025-04-16T23:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 2.75e-7, + input_cache_read: 2.75e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "gemini-2.0-flash", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash)(@[a-zA-Z0-9]+)?$", + startDate: "2025-04-22T10:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1e-7, + output: 4e-7, + }, + }, + ], + }, + { + modelName: "gemini-2.0-flash-lite-preview", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview)(@[a-zA-Z0-9]+)?$", + startDate: "2025-04-22T10:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 7.5e-8, + output: 3e-7, + }, + }, + ], + }, + { + modelName: "gpt-4.1-nano", + matchPattern: "(?i)^(openai/)?(gpt-4.1-nano)$", + startDate: "2025-04-22T10:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1e-7, + input_cached_tokens: 2.5e-8, + input_cached_text_tokens: 2.5e-8, + input_cache_read: 2.5e-8, + output: 4e-7, + }, + }, + ], + }, + { + modelName: "gpt-4.1-mini", + matchPattern: "(?i)^(openai/)?(gpt-4.1-mini)$", + startDate: "2025-04-22T10:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 4e-7, + input_cached_tokens: 1e-7, + input_cached_text_tokens: 1e-7, + input_cache_read: 1e-7, + output: 0.0000016, + }, + }, + ], + }, + { + modelName: "claude-sonnet-4-5-20250929", + matchPattern: + "(?i)^(anthropic/)?(claude-sonnet-4-5(-20250929)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-5(-20250929)?-v1(:0)?|claude-sonnet-4-5-V1(@20250929)?|claude-sonnet-4-5(@20250929)?)$", + startDate: "2025-09-29T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + { + name: "Large Context", + isDefault: false, + priority: 1, + conditions: [ { - "usageDetailPattern": "input", - "operator": "gt", - "value": 200000 - } + usageDetailPattern: "input", + operator: "gt", + value: 200000, + }, ], - "prices": { - "input": 0.000006, - "input_tokens": 0.000006, - "output": 0.0000225, - "output_tokens": 0.0000225, - "cache_creation_input_tokens": 0.0000075, - "input_cache_creation": 0.0000075, - "input_cache_creation_5m": 0.0000075, - "input_cache_creation_1h": 0.000012, - "cache_read_input_tokens": 6e-7, - "input_cache_read": 6e-7 - } - } - ] - }, - { - "modelName": "claude-sonnet-4-20250514", - "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4(-20250514)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4(-20250514)?-v1(:0)?|claude-sonnet-4-V1(@20250514)?|claude-sonnet-4(@20250514)?)$", - "startDate": "2025-05-22T17:09:02.131Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-sonnet-4-latest", - "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4-latest)$", - "startDate": "2025-05-22T17:09:02.131Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-opus-4-20250514", - "matchPattern": "(?i)^(anthropic/)?(claude-opus-4(-20250514)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4(-20250514)?-v1(:0)?|claude-opus-4(@20250514)?)$", - "startDate": "2025-05-22T17:09:02.131Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_tokens": 0.000015, - "output": 0.000075, - "output_tokens": 0.000075, - "cache_creation_input_tokens": 0.00001875, - "input_cache_creation": 0.00001875, - "input_cache_creation_5m": 0.00001875, - "input_cache_creation_1h": 0.00003, - "cache_read_input_tokens": 0.0000015, - "input_cache_read": 0.0000015 - } - } - ] - }, - { - "modelName": "o3-pro", - "matchPattern": "(?i)^(openai/)?(o3-pro)$", - "startDate": "2025-06-10T22:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00002, - "output": 0.00008, - "output_reasoning_tokens": 0.00008, - "output_reasoning": 0.00008 - } - } - ] - }, - { - "modelName": "o3-pro-2025-06-10", - "matchPattern": "(?i)^(openai/)?(o3-pro-2025-06-10)$", - "startDate": "2025-06-10T22:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00002, - "output": 0.00008, - "output_reasoning_tokens": 0.00008, - "output_reasoning": 0.00008 - } - } - ] - }, - { - "modelName": "o1-pro", - "matchPattern": "(?i)^(openai/)?(o1-pro)$", - "startDate": "2025-06-10T22:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00015, - "output": 0.0006, - "output_reasoning_tokens": 0.0006, - "output_reasoning": 0.0006 - } - } - ] - }, - { - "modelName": "o1-pro-2025-03-19", - "matchPattern": "(?i)^(openai/)?(o1-pro-2025-03-19)$", - "startDate": "2025-06-10T22:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00015, - "output": 0.0006, - "output_reasoning_tokens": 0.0006, - "output_reasoning": 0.0006 - } - } - ] - }, - { - "modelName": "gemini-2.5-flash", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.5-flash)$", - "startDate": "2025-07-03T13:44:06.964Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 3e-7, - "input_text": 3e-7, - "input_modality_1": 3e-7, - "prompt_token_count": 3e-7, - "promptTokenCount": 3e-7, - "input_cached_tokens": 3e-8, - "cached_content_token_count": 3e-8, - "output": 0.0000025, - "output_text": 0.0000025, - "output_modality_1": 0.0000025, - "candidates_token_count": 0.0000025, - "candidatesTokenCount": 0.0000025, - "thoughtsTokenCount": 0.0000025, - "thoughts_token_count": 0.0000025, - "output_reasoning": 0.0000025, - "input_audio_tokens": 0.000001 - } - } - ] - }, - { - "modelName": "gemini-2.5-flash-lite", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.5-flash-lite)$", - "startDate": "2025-07-03T13:44:06.964Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1e-7, - "input_text": 1e-7, - "input_modality_1": 1e-7, - "prompt_token_count": 1e-7, - "promptTokenCount": 1e-7, - "input_cached_tokens": 2.5e-8, - "cached_content_token_count": 2.5e-8, - "output": 4e-7, - "output_text": 4e-7, - "output_modality_1": 4e-7, - "candidates_token_count": 4e-7, - "candidatesTokenCount": 4e-7, - "thoughtsTokenCount": 4e-7, - "thoughts_token_count": 4e-7, - "output_reasoning": 4e-7, - "input_audio_tokens": 5e-7 - } - } - ] - }, - { - "modelName": "claude-opus-4-1-20250805", - "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-1(-20250805)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-1(-20250805)?-v1(:0)?|claude-opus-4-1(@20250805)?)$", - "startDate": "2025-08-05T15:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_tokens": 0.000015, - "output": 0.000075, - "output_tokens": 0.000075, - "cache_creation_input_tokens": 0.00001875, - "input_cache_creation": 0.00001875, - "input_cache_creation_5m": 0.00001875, - "input_cache_creation_1h": 0.00003, - "cache_read_input_tokens": 0.0000015, - "input_cache_read": 0.0000015 - } - } - ] - }, - { - "modelName": "gpt-5", - "matchPattern": "(?i)^(openai/)?(gpt-5)$", - "startDate": "2025-08-07T16:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_cached_tokens": 1.25e-7, - "output": 0.00001, - "input_cache_read": 1.25e-7, - "output_reasoning_tokens": 0.00001, - "output_reasoning": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-5-2025-08-07", - "matchPattern": "(?i)^(openai/)?(gpt-5-2025-08-07)$", - "startDate": "2025-08-11T08:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_cached_tokens": 1.25e-7, - "output": 0.00001, - "input_cache_read": 1.25e-7, - "output_reasoning_tokens": 0.00001, - "output_reasoning": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-5-mini", - "matchPattern": "(?i)^(openai/)?(gpt-5-mini)$", - "startDate": "2025-08-07T16:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "input_cached_tokens": 2.5e-8, - "output": 0.000002, - "input_cache_read": 2.5e-8, - "output_reasoning_tokens": 0.000002, - "output_reasoning": 0.000002 - } - } - ] - }, - { - "modelName": "gpt-5-mini-2025-08-07", - "matchPattern": "(?i)^(openai/)?(gpt-5-mini-2025-08-07)$", - "startDate": "2025-08-11T08:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "input_cached_tokens": 2.5e-8, - "output": 0.000002, - "input_cache_read": 2.5e-8, - "output_reasoning_tokens": 0.000002, - "output_reasoning": 0.000002 - } - } - ] - }, - { - "modelName": "gpt-5-nano", - "matchPattern": "(?i)^(openai/)?(gpt-5-nano)$", - "startDate": "2025-08-07T16:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-8, - "input_cached_tokens": 5e-9, - "output": 4e-7, - "input_cache_read": 5e-9, - "output_reasoning_tokens": 4e-7, - "output_reasoning": 4e-7 - } - } - ] - }, - { - "modelName": "gpt-5-nano-2025-08-07", - "matchPattern": "(?i)^(openai/)?(gpt-5-nano-2025-08-07)$", - "startDate": "2025-08-11T08:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-8, - "input_cached_tokens": 5e-9, - "output": 4e-7, - "input_cache_read": 5e-9, - "output_reasoning_tokens": 4e-7, - "output_reasoning": 4e-7 - } - } - ] - }, - { - "modelName": "gpt-5-chat-latest", - "matchPattern": "(?i)^(openai/)?(gpt-5-chat-latest)$", - "startDate": "2025-08-07T16:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_cached_tokens": 1.25e-7, - "output": 0.00001, - "input_cache_read": 1.25e-7, - "output_reasoning_tokens": 0.00001, - "output_reasoning": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-5-pro", - "matchPattern": "(?i)^(openai/)?(gpt-5-pro)$", - "startDate": "2025-10-07T08:03:54.727Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "output": 0.00012, - "output_reasoning_tokens": 0.00012, - "output_reasoning": 0.00012 - } - } - ] - }, - { - "modelName": "gpt-5-pro-2025-10-06", - "matchPattern": "(?i)^(openai/)?(gpt-5-pro-2025-10-06)$", - "startDate": "2025-10-07T08:03:54.727Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "output": 0.00012, - "output_reasoning_tokens": 0.00012, - "output_reasoning": 0.00012 - } - } - ] - }, - { - "modelName": "claude-haiku-4-5-20251001", - "matchPattern": "(?i)^(anthropic/)?(claude-haiku-4-5-20251001|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-haiku-4-5-20251001-v1:0|claude-4-5-haiku@20251001)$", - "startDate": "2025-10-16T08:20:44.558Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000001, - "input_tokens": 0.000001, - "output": 0.000005, - "output_tokens": 0.000005, - "cache_creation_input_tokens": 0.00000125, - "input_cache_creation": 0.00000125, - "input_cache_creation_5m": 0.00000125, - "input_cache_creation_1h": 0.000002, - "cache_read_input_tokens": 1e-7, - "input_cache_read": 1e-7 - } - } - ] - }, - { - "modelName": "gpt-5.1", - "matchPattern": "(?i)^(openai/)?(gpt-5.1)$", - "startDate": "2025-11-14T08:57:23.481Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_cached_tokens": 1.25e-7, - "output": 0.00001, - "input_cache_read": 1.25e-7, - "output_reasoning_tokens": 0.00001, - "output_reasoning": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-5.1-2025-11-13", - "matchPattern": "(?i)^(openai/)?(gpt-5.1-2025-11-13)$", - "startDate": "2025-11-14T08:57:23.481Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_cached_tokens": 1.25e-7, - "output": 0.00001, - "input_cache_read": 1.25e-7, - "output_reasoning_tokens": 0.00001, - "output_reasoning": 0.00001 - } - } - ] - }, - { - "modelName": "claude-opus-4-5-20251101", - "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-5(-20251101)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-5(-20251101)?-v1(:0)?|claude-opus-4-5(@20251101)?)$", - "startDate": "2025-11-24T20:53:27.571Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000005, - "input_tokens": 0.000005, - "output": 0.000025, - "output_tokens": 0.000025, - "cache_creation_input_tokens": 0.00000625, - "input_cache_creation": 0.00000625, - "input_cache_creation_5m": 0.00000625, - "input_cache_creation_1h": 0.00001, - "cache_read_input_tokens": 5e-7, - "input_cache_read": 5e-7 - } - } - ] - }, - { - "modelName": "claude-sonnet-4-6", - "matchPattern": "(?i)^(anthropic\\/)?(claude-sonnet-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-6(-v1(:0)?)?|claude-sonnet-4-6)$", - "startDate": "2026-02-18T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - }, - { - "name": "Large Context", - "isDefault": false, - "priority": 1, - "conditions": [ + prices: { + input: 0.000006, + input_tokens: 0.000006, + output: 0.0000225, + output_tokens: 0.0000225, + cache_creation_input_tokens: 0.0000075, + input_cache_creation: 0.0000075, + input_cache_creation_5m: 0.0000075, + input_cache_creation_1h: 0.000012, + cache_read_input_tokens: 6e-7, + input_cache_read: 6e-7, + }, + }, + ], + }, + { + modelName: "claude-sonnet-4-20250514", + matchPattern: + "(?i)^(anthropic/)?(claude-sonnet-4(-20250514)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4(-20250514)?-v1(:0)?|claude-sonnet-4-V1(@20250514)?|claude-sonnet-4(@20250514)?)$", + startDate: "2025-05-22T17:09:02.131Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-sonnet-4-latest", + matchPattern: "(?i)^(anthropic/)?(claude-sonnet-4-latest)$", + startDate: "2025-05-22T17:09:02.131Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-opus-4-20250514", + matchPattern: + "(?i)^(anthropic/)?(claude-opus-4(-20250514)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4(-20250514)?-v1(:0)?|claude-opus-4(@20250514)?)$", + startDate: "2025-05-22T17:09:02.131Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_tokens: 0.000015, + output: 0.000075, + output_tokens: 0.000075, + cache_creation_input_tokens: 0.00001875, + input_cache_creation: 0.00001875, + input_cache_creation_5m: 0.00001875, + input_cache_creation_1h: 0.00003, + cache_read_input_tokens: 0.0000015, + input_cache_read: 0.0000015, + }, + }, + ], + }, + { + modelName: "o3-pro", + matchPattern: "(?i)^(openai/)?(o3-pro)$", + startDate: "2025-06-10T22:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00002, + output: 0.00008, + output_reasoning_tokens: 0.00008, + output_reasoning: 0.00008, + }, + }, + ], + }, + { + modelName: "o3-pro-2025-06-10", + matchPattern: "(?i)^(openai/)?(o3-pro-2025-06-10)$", + startDate: "2025-06-10T22:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00002, + output: 0.00008, + output_reasoning_tokens: 0.00008, + output_reasoning: 0.00008, + }, + }, + ], + }, + { + modelName: "o1-pro", + matchPattern: "(?i)^(openai/)?(o1-pro)$", + startDate: "2025-06-10T22:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00015, + output: 0.0006, + output_reasoning_tokens: 0.0006, + output_reasoning: 0.0006, + }, + }, + ], + }, + { + modelName: "o1-pro-2025-03-19", + matchPattern: "(?i)^(openai/)?(o1-pro-2025-03-19)$", + startDate: "2025-06-10T22:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00015, + output: 0.0006, + output_reasoning_tokens: 0.0006, + output_reasoning: 0.0006, + }, + }, + ], + }, + { + modelName: "gemini-2.5-flash", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.5-flash)$", + startDate: "2025-07-03T13:44:06.964Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 3e-7, + input_text: 3e-7, + input_modality_1: 3e-7, + prompt_token_count: 3e-7, + promptTokenCount: 3e-7, + input_cached_tokens: 3e-8, + cached_content_token_count: 3e-8, + output: 0.0000025, + output_text: 0.0000025, + output_modality_1: 0.0000025, + candidates_token_count: 0.0000025, + candidatesTokenCount: 0.0000025, + thoughtsTokenCount: 0.0000025, + thoughts_token_count: 0.0000025, + output_reasoning: 0.0000025, + input_audio_tokens: 0.000001, + }, + }, + ], + }, + { + modelName: "gemini-2.5-flash-lite", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.5-flash-lite)$", + startDate: "2025-07-03T13:44:06.964Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1e-7, + input_text: 1e-7, + input_modality_1: 1e-7, + prompt_token_count: 1e-7, + promptTokenCount: 1e-7, + input_cached_tokens: 2.5e-8, + cached_content_token_count: 2.5e-8, + output: 4e-7, + output_text: 4e-7, + output_modality_1: 4e-7, + candidates_token_count: 4e-7, + candidatesTokenCount: 4e-7, + thoughtsTokenCount: 4e-7, + thoughts_token_count: 4e-7, + output_reasoning: 4e-7, + input_audio_tokens: 5e-7, + }, + }, + ], + }, + { + modelName: "claude-opus-4-1-20250805", + matchPattern: + "(?i)^(anthropic/)?(claude-opus-4-1(-20250805)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-1(-20250805)?-v1(:0)?|claude-opus-4-1(@20250805)?)$", + startDate: "2025-08-05T15:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_tokens: 0.000015, + output: 0.000075, + output_tokens: 0.000075, + cache_creation_input_tokens: 0.00001875, + input_cache_creation: 0.00001875, + input_cache_creation_5m: 0.00001875, + input_cache_creation_1h: 0.00003, + cache_read_input_tokens: 0.0000015, + input_cache_read: 0.0000015, + }, + }, + ], + }, + { + modelName: "gpt-5", + matchPattern: "(?i)^(openai/)?(gpt-5)$", + startDate: "2025-08-07T16:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_cached_tokens: 1.25e-7, + output: 0.00001, + input_cache_read: 1.25e-7, + output_reasoning_tokens: 0.00001, + output_reasoning: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-5-2025-08-07", + matchPattern: "(?i)^(openai/)?(gpt-5-2025-08-07)$", + startDate: "2025-08-11T08:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_cached_tokens: 1.25e-7, + output: 0.00001, + input_cache_read: 1.25e-7, + output_reasoning_tokens: 0.00001, + output_reasoning: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-5-mini", + matchPattern: "(?i)^(openai/)?(gpt-5-mini)$", + startDate: "2025-08-07T16:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + input_cached_tokens: 2.5e-8, + output: 0.000002, + input_cache_read: 2.5e-8, + output_reasoning_tokens: 0.000002, + output_reasoning: 0.000002, + }, + }, + ], + }, + { + modelName: "gpt-5-mini-2025-08-07", + matchPattern: "(?i)^(openai/)?(gpt-5-mini-2025-08-07)$", + startDate: "2025-08-11T08:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + input_cached_tokens: 2.5e-8, + output: 0.000002, + input_cache_read: 2.5e-8, + output_reasoning_tokens: 0.000002, + output_reasoning: 0.000002, + }, + }, + ], + }, + { + modelName: "gpt-5-nano", + matchPattern: "(?i)^(openai/)?(gpt-5-nano)$", + startDate: "2025-08-07T16:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-8, + input_cached_tokens: 5e-9, + output: 4e-7, + input_cache_read: 5e-9, + output_reasoning_tokens: 4e-7, + output_reasoning: 4e-7, + }, + }, + ], + }, + { + modelName: "gpt-5-nano-2025-08-07", + matchPattern: "(?i)^(openai/)?(gpt-5-nano-2025-08-07)$", + startDate: "2025-08-11T08:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-8, + input_cached_tokens: 5e-9, + output: 4e-7, + input_cache_read: 5e-9, + output_reasoning_tokens: 4e-7, + output_reasoning: 4e-7, + }, + }, + ], + }, + { + modelName: "gpt-5-chat-latest", + matchPattern: "(?i)^(openai/)?(gpt-5-chat-latest)$", + startDate: "2025-08-07T16:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_cached_tokens: 1.25e-7, + output: 0.00001, + input_cache_read: 1.25e-7, + output_reasoning_tokens: 0.00001, + output_reasoning: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-5-pro", + matchPattern: "(?i)^(openai/)?(gpt-5-pro)$", + startDate: "2025-10-07T08:03:54.727Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + output: 0.00012, + output_reasoning_tokens: 0.00012, + output_reasoning: 0.00012, + }, + }, + ], + }, + { + modelName: "gpt-5-pro-2025-10-06", + matchPattern: "(?i)^(openai/)?(gpt-5-pro-2025-10-06)$", + startDate: "2025-10-07T08:03:54.727Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + output: 0.00012, + output_reasoning_tokens: 0.00012, + output_reasoning: 0.00012, + }, + }, + ], + }, + { + modelName: "claude-haiku-4-5-20251001", + matchPattern: + "(?i)^(anthropic/)?(claude-haiku-4-5-20251001|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-haiku-4-5-20251001-v1:0|claude-4-5-haiku@20251001)$", + startDate: "2025-10-16T08:20:44.558Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000001, + input_tokens: 0.000001, + output: 0.000005, + output_tokens: 0.000005, + cache_creation_input_tokens: 0.00000125, + input_cache_creation: 0.00000125, + input_cache_creation_5m: 0.00000125, + input_cache_creation_1h: 0.000002, + cache_read_input_tokens: 1e-7, + input_cache_read: 1e-7, + }, + }, + ], + }, + { + modelName: "gpt-5.1", + matchPattern: "(?i)^(openai/)?(gpt-5.1)$", + startDate: "2025-11-14T08:57:23.481Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_cached_tokens: 1.25e-7, + output: 0.00001, + input_cache_read: 1.25e-7, + output_reasoning_tokens: 0.00001, + output_reasoning: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-5.1-2025-11-13", + matchPattern: "(?i)^(openai/)?(gpt-5.1-2025-11-13)$", + startDate: "2025-11-14T08:57:23.481Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_cached_tokens: 1.25e-7, + output: 0.00001, + input_cache_read: 1.25e-7, + output_reasoning_tokens: 0.00001, + output_reasoning: 0.00001, + }, + }, + ], + }, + { + modelName: "claude-opus-4-5-20251101", + matchPattern: + "(?i)^(anthropic/)?(claude-opus-4-5(-20251101)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-5(-20251101)?-v1(:0)?|claude-opus-4-5(@20251101)?)$", + startDate: "2025-11-24T20:53:27.571Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000005, + input_tokens: 0.000005, + output: 0.000025, + output_tokens: 0.000025, + cache_creation_input_tokens: 0.00000625, + input_cache_creation: 0.00000625, + input_cache_creation_5m: 0.00000625, + input_cache_creation_1h: 0.00001, + cache_read_input_tokens: 5e-7, + input_cache_read: 5e-7, + }, + }, + ], + }, + { + modelName: "claude-sonnet-4-6", + matchPattern: + "(?i)^(anthropic\\/)?(claude-sonnet-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-6(-v1(:0)?)?|claude-sonnet-4-6)$", + startDate: "2026-02-18T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + { + name: "Large Context", + isDefault: false, + priority: 1, + conditions: [ { - "usageDetailPattern": "input", - "operator": "gt", - "value": 200000 - } + usageDetailPattern: "input", + operator: "gt", + value: 200000, + }, ], - "prices": { - "input": 0.000006, - "input_tokens": 0.000006, - "output": 0.0000225, - "output_tokens": 0.0000225, - "cache_creation_input_tokens": 0.0000075, - "input_cache_creation": 0.0000075, - "input_cache_creation_5m": 0.0000075, - "input_cache_creation_1h": 0.000012, - "cache_read_input_tokens": 6e-7, - "input_cache_read": 6e-7 - } - } - ] - }, - { - "modelName": "claude-opus-4-6", - "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$", - "startDate": "2026-02-09T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000005, - "input_tokens": 0.000005, - "output": 0.000025, - "output_tokens": 0.000025, - "cache_creation_input_tokens": 0.00000625, - "input_cache_creation": 0.00000625, - "input_cache_creation_5m": 0.00000625, - "input_cache_creation_1h": 0.00001, - "cache_read_input_tokens": 5e-7, - "input_cache_read": 5e-7 - } - } - ] - }, - { - "modelName": "gemini-2.5-pro", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.5-pro)$", - "startDate": "2025-11-26T13:27:53.545Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_text": 0.00000125, - "input_modality_1": 0.00000125, - "prompt_token_count": 0.00000125, - "promptTokenCount": 0.00000125, - "input_cached_tokens": 1.25e-7, - "cached_content_token_count": 1.25e-7, - "output": 0.00001, - "output_text": 0.00001, - "output_modality_1": 0.00001, - "candidates_token_count": 0.00001, - "candidatesTokenCount": 0.00001, - "thoughtsTokenCount": 0.00001, - "thoughts_token_count": 0.00001, - "output_reasoning": 0.00001 - } - }, - { - "name": "Large Context", - "isDefault": false, - "priority": 1, - "conditions": [ + prices: { + input: 0.000006, + input_tokens: 0.000006, + output: 0.0000225, + output_tokens: 0.0000225, + cache_creation_input_tokens: 0.0000075, + input_cache_creation: 0.0000075, + input_cache_creation_5m: 0.0000075, + input_cache_creation_1h: 0.000012, + cache_read_input_tokens: 6e-7, + input_cache_read: 6e-7, + }, + }, + ], + }, + { + modelName: "claude-opus-4-6", + matchPattern: + "(?i)^(anthropic/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$", + startDate: "2026-02-09T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000005, + input_tokens: 0.000005, + output: 0.000025, + output_tokens: 0.000025, + cache_creation_input_tokens: 0.00000625, + input_cache_creation: 0.00000625, + input_cache_creation_5m: 0.00000625, + input_cache_creation_1h: 0.00001, + cache_read_input_tokens: 5e-7, + input_cache_read: 5e-7, + }, + }, + ], + }, + { + modelName: "gemini-2.5-pro", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.5-pro)$", + startDate: "2025-11-26T13:27:53.545Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_text: 0.00000125, + input_modality_1: 0.00000125, + prompt_token_count: 0.00000125, + promptTokenCount: 0.00000125, + input_cached_tokens: 1.25e-7, + cached_content_token_count: 1.25e-7, + output: 0.00001, + output_text: 0.00001, + output_modality_1: 0.00001, + candidates_token_count: 0.00001, + candidatesTokenCount: 0.00001, + thoughtsTokenCount: 0.00001, + thoughts_token_count: 0.00001, + output_reasoning: 0.00001, + }, + }, + { + name: "Large Context", + isDefault: false, + priority: 1, + conditions: [ { - "usageDetailPattern": "(input|prompt|cached)", - "operator": "gt", - "value": 200000 - } + usageDetailPattern: "(input|prompt|cached)", + operator: "gt", + value: 200000, + }, ], - "prices": { - "input": 0.0000025, - "input_text": 0.0000025, - "input_modality_1": 0.0000025, - "prompt_token_count": 0.0000025, - "promptTokenCount": 0.0000025, - "input_cached_tokens": 2.5e-7, - "cached_content_token_count": 2.5e-7, - "output": 0.000015, - "output_text": 0.000015, - "output_modality_1": 0.000015, - "candidates_token_count": 0.000015, - "candidatesTokenCount": 0.000015, - "thoughtsTokenCount": 0.000015, - "thoughts_token_count": 0.000015, - "output_reasoning": 0.000015 - } - } - ] - }, - { - "modelName": "gemini-3-pro-preview", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-3-pro-preview)$", - "startDate": "2025-11-26T13:27:53.545Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_text": 0.000002, - "input_modality_1": 0.000002, - "prompt_token_count": 0.000002, - "promptTokenCount": 0.000002, - "input_cached_tokens": 2e-7, - "cached_content_token_count": 2e-7, - "output": 0.000012, - "output_text": 0.000012, - "output_modality_1": 0.000012, - "candidates_token_count": 0.000012, - "candidatesTokenCount": 0.000012, - "thoughtsTokenCount": 0.000012, - "thoughts_token_count": 0.000012, - "output_reasoning": 0.000012 - } - }, - { - "name": "Large Context", - "isDefault": false, - "priority": 1, - "conditions": [ + prices: { + input: 0.0000025, + input_text: 0.0000025, + input_modality_1: 0.0000025, + prompt_token_count: 0.0000025, + promptTokenCount: 0.0000025, + input_cached_tokens: 2.5e-7, + cached_content_token_count: 2.5e-7, + output: 0.000015, + output_text: 0.000015, + output_modality_1: 0.000015, + candidates_token_count: 0.000015, + candidatesTokenCount: 0.000015, + thoughtsTokenCount: 0.000015, + thoughts_token_count: 0.000015, + output_reasoning: 0.000015, + }, + }, + ], + }, + { + modelName: "gemini-3-pro-preview", + matchPattern: "(?i)^(google(ai)?/)?(gemini-3-pro-preview)$", + startDate: "2025-11-26T13:27:53.545Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_text: 0.000002, + input_modality_1: 0.000002, + prompt_token_count: 0.000002, + promptTokenCount: 0.000002, + input_cached_tokens: 2e-7, + cached_content_token_count: 2e-7, + output: 0.000012, + output_text: 0.000012, + output_modality_1: 0.000012, + candidates_token_count: 0.000012, + candidatesTokenCount: 0.000012, + thoughtsTokenCount: 0.000012, + thoughts_token_count: 0.000012, + output_reasoning: 0.000012, + }, + }, + { + name: "Large Context", + isDefault: false, + priority: 1, + conditions: [ { - "usageDetailPattern": "(input|prompt|cached)", - "operator": "gt", - "value": 200000 - } + usageDetailPattern: "(input|prompt|cached)", + operator: "gt", + value: 200000, + }, ], - "prices": { - "input": 0.000004, - "input_text": 0.000004, - "input_modality_1": 0.000004, - "prompt_token_count": 0.000004, - "promptTokenCount": 0.000004, - "input_cached_tokens": 4e-7, - "cached_content_token_count": 4e-7, - "output": 0.000018, - "output_text": 0.000018, - "output_modality_1": 0.000018, - "candidates_token_count": 0.000018, - "candidatesTokenCount": 0.000018, - "thoughtsTokenCount": 0.000018, - "thoughts_token_count": 0.000018, - "output_reasoning": 0.000018 - } - } - ] - }, - { - "modelName": "gemini-3.1-pro-preview", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-3.1-pro-preview(-customtools)?)$", - "startDate": "2026-02-19T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_modality_1": 0.000002, - "input_text": 0.000002, - "prompt_token_count": 0.000002, - "promptTokenCount": 0.000002, - "input_cached_tokens": 2e-7, - "cached_content_token_count": 2e-7, - "output": 0.000012, - "output_text": 0.000012, - "output_modality_1": 0.000012, - "candidates_token_count": 0.000012, - "candidatesTokenCount": 0.000012, - "thoughtsTokenCount": 0.000012, - "thoughts_token_count": 0.000012, - "output_reasoning": 0.000012 - } - }, - { - "name": "Large Context", - "isDefault": false, - "priority": 1, - "conditions": [ + prices: { + input: 0.000004, + input_text: 0.000004, + input_modality_1: 0.000004, + prompt_token_count: 0.000004, + promptTokenCount: 0.000004, + input_cached_tokens: 4e-7, + cached_content_token_count: 4e-7, + output: 0.000018, + output_text: 0.000018, + output_modality_1: 0.000018, + candidates_token_count: 0.000018, + candidatesTokenCount: 0.000018, + thoughtsTokenCount: 0.000018, + thoughts_token_count: 0.000018, + output_reasoning: 0.000018, + }, + }, + ], + }, + { + modelName: "gemini-3.1-pro-preview", + matchPattern: "(?i)^(google(ai)?/)?(gemini-3.1-pro-preview(-customtools)?)$", + startDate: "2026-02-19T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_modality_1: 0.000002, + input_text: 0.000002, + prompt_token_count: 0.000002, + promptTokenCount: 0.000002, + input_cached_tokens: 2e-7, + cached_content_token_count: 2e-7, + output: 0.000012, + output_text: 0.000012, + output_modality_1: 0.000012, + candidates_token_count: 0.000012, + candidatesTokenCount: 0.000012, + thoughtsTokenCount: 0.000012, + thoughts_token_count: 0.000012, + output_reasoning: 0.000012, + }, + }, + { + name: "Large Context", + isDefault: false, + priority: 1, + conditions: [ { - "usageDetailPattern": "(input|prompt|cached)", - "operator": "gt", - "value": 200000 - } + usageDetailPattern: "(input|prompt|cached)", + operator: "gt", + value: 200000, + }, ], - "prices": { - "input": 0.000004, - "input_modality_1": 0.000004, - "input_text": 0.000004, - "prompt_token_count": 0.000004, - "promptTokenCount": 0.000004, - "input_cached_tokens": 4e-7, - "cached_content_token_count": 4e-7, - "output": 0.000018, - "output_text": 0.000018, - "output_modality_1": 0.000018, - "candidates_token_count": 0.000018, - "candidatesTokenCount": 0.000018, - "thoughtsTokenCount": 0.000018, - "thoughts_token_count": 0.000018, - "output_reasoning": 0.000018 - } - } - ] - }, - { - "modelName": "gpt-5.2", - "matchPattern": "(?i)^(openai/)?(gpt-5.2)$", - "startDate": "2025-12-12T09:00:06.513Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000175, - "input_cached_tokens": 1.75e-7, - "input_cache_read": 1.75e-7, - "output": 0.000014, - "output_reasoning_tokens": 0.000014, - "output_reasoning": 0.000014 - } - } - ] - }, - { - "modelName": "gpt-5.2-2025-12-11", - "matchPattern": "(?i)^(openai/)?(gpt-5.2-2025-12-11)$", - "startDate": "2025-12-12T09:00:06.513Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000175, - "input_cached_tokens": 1.75e-7, - "input_cache_read": 1.75e-7, - "output": 0.000014, - "output_reasoning_tokens": 0.000014, - "output_reasoning": 0.000014 - } - } - ] - }, - { - "modelName": "gpt-5.2-pro", - "matchPattern": "(?i)^(openai/)?(gpt-5.2-pro)$", - "startDate": "2025-12-12T09:00:06.513Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000021, - "output": 0.000168, - "output_reasoning_tokens": 0.000168, - "output_reasoning": 0.000168 - } - } - ] - }, - { - "modelName": "gpt-5.2-pro-2025-12-11", - "matchPattern": "(?i)^(openai/)?(gpt-5.2-pro-2025-12-11)$", - "startDate": "2025-12-12T09:00:06.513Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000021, - "output": 0.000168, - "output_reasoning_tokens": 0.000168, - "output_reasoning": 0.000168 - } - } - ] - }, - { - "modelName": "gpt-5.4", - "matchPattern": "(?i)^(openai/)?(gpt-5.4)$", - "startDate": "2026-03-05T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "input_cached_tokens": 2.5e-7, - "input_cache_read": 2.5e-7, - "output": 0.000015, - "output_reasoning_tokens": 0.000015, - "output_reasoning": 0.000015 - } - } - ] - }, - { - "modelName": "gpt-5.4-pro", - "matchPattern": "(?i)^(openai/)?(gpt-5.4-pro)$", - "startDate": "2026-03-05T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00003, - "output": 0.00018, - "output_reasoning_tokens": 0.00018, - "output_reasoning": 0.00018 - } - } - ] - }, - { - "modelName": "gpt-5.4-2026-03-05", - "matchPattern": "(?i)^(openai/)?(gpt-5.4-2026-03-05)$", - "startDate": "2026-03-05T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "input_cached_tokens": 2.5e-7, - "input_cache_read": 2.5e-7, - "output": 0.000015, - "output_reasoning_tokens": 0.000015, - "output_reasoning": 0.000015 - } - } - ] - }, - { - "modelName": "gpt-5.4-pro-2026-03-05", - "matchPattern": "(?i)^(openai/)?(gpt-5.4-pro-2026-03-05)$", - "startDate": "2026-03-05T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00003, - "output": 0.00018, - "output_reasoning_tokens": 0.00018, - "output_reasoning": 0.00018 - } - } - ] - }, - { - "modelName": "gpt-5.4-mini", - "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-mini)$", - "startDate": "2026-03-18T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 7.5e-7, - "input_cached_tokens": 7.5e-8, - "input_cache_read": 7.5e-8, - "output": 0.0000045 - } - } - ] - }, - { - "modelName": "gpt-5.4-mini-2026-03-17", - "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-mini-2026-03-17)$", - "startDate": "2026-03-18T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 7.5e-7, - "input_cached_tokens": 7.5e-8, - "input_cache_read": 7.5e-8, - "output": 0.0000045 - } - } - ] - }, - { - "modelName": "gpt-5.4-nano", - "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-nano)$", - "startDate": "2026-03-18T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2e-7, - "input_cached_tokens": 2e-8, - "input_cache_read": 2e-8, - "output": 0.00000125 - } - } - ] - }, - { - "modelName": "gpt-5.4-nano-2026-03-17", - "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-nano-2026-03-17)$", - "startDate": "2026-03-18T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2e-7, - "input_cached_tokens": 2e-8, - "input_cache_read": 2e-8, - "output": 0.00000125 - } - } - ] - }, - { - "modelName": "gemini-3-flash-preview", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-3-flash-preview)$", - "startDate": "2025-12-21T12:01:42.282Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-7, - "input_text": 5e-7, - "input_modality_1": 5e-7, - "prompt_token_count": 5e-7, - "promptTokenCount": 5e-7, - "input_cached_tokens": 5e-8, - "cached_content_token_count": 5e-8, - "output": 0.000003, - "output_text": 0.000003, - "output_modality_1": 0.000003, - "candidates_token_count": 0.000003, - "candidatesTokenCount": 0.000003, - "thoughtsTokenCount": 0.000003, - "thoughts_token_count": 0.000003, - "output_reasoning": 0.000003 - } - } - ] - }, - { - "modelName": "gemini-3.1-flash-lite-preview", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-3.1-flash-lite-preview)$", - "startDate": "2026-03-03T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "input_modality_1": 2.5e-7, - "input_text": 2.5e-7, - "prompt_token_count": 2.5e-7, - "promptTokenCount": 2.5e-7, - "input_cached_tokens": 2.5e-8, - "cached_content_token_count": 2.5e-8, - "output": 0.0000015, - "output_text": 0.0000015, - "output_modality_1": 0.0000015, - "candidates_token_count": 0.0000015, - "candidatesTokenCount": 0.0000015, - "thoughtsTokenCount": 0.0000015, - "thoughts_token_count": 0.0000015, - "output_reasoning": 0.0000015, - "input_audio_tokens": 5e-7 - } - } - ] - }, - { - "modelName": "gemini-live-2.5-flash-native-audio", - "matchPattern": "(?i)^(google/)?(gemini-live-2.5-flash-native-audio)$", - "startDate": "2026-03-16T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input_text": 5e-7, - "input_audio": 0.000003, - "input_image": 0.000003, - "output_text": 0.000002, - "output_audio": 0.000012 - } - } - ] - } + prices: { + input: 0.000004, + input_modality_1: 0.000004, + input_text: 0.000004, + prompt_token_count: 0.000004, + promptTokenCount: 0.000004, + input_cached_tokens: 4e-7, + cached_content_token_count: 4e-7, + output: 0.000018, + output_text: 0.000018, + output_modality_1: 0.000018, + candidates_token_count: 0.000018, + candidatesTokenCount: 0.000018, + thoughtsTokenCount: 0.000018, + thoughts_token_count: 0.000018, + output_reasoning: 0.000018, + }, + }, + ], + }, + { + modelName: "gpt-5.2", + matchPattern: "(?i)^(openai/)?(gpt-5.2)$", + startDate: "2025-12-12T09:00:06.513Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000175, + input_cached_tokens: 1.75e-7, + input_cache_read: 1.75e-7, + output: 0.000014, + output_reasoning_tokens: 0.000014, + output_reasoning: 0.000014, + }, + }, + ], + }, + { + modelName: "gpt-5.2-2025-12-11", + matchPattern: "(?i)^(openai/)?(gpt-5.2-2025-12-11)$", + startDate: "2025-12-12T09:00:06.513Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000175, + input_cached_tokens: 1.75e-7, + input_cache_read: 1.75e-7, + output: 0.000014, + output_reasoning_tokens: 0.000014, + output_reasoning: 0.000014, + }, + }, + ], + }, + { + modelName: "gpt-5.2-pro", + matchPattern: "(?i)^(openai/)?(gpt-5.2-pro)$", + startDate: "2025-12-12T09:00:06.513Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000021, + output: 0.000168, + output_reasoning_tokens: 0.000168, + output_reasoning: 0.000168, + }, + }, + ], + }, + { + modelName: "gpt-5.2-pro-2025-12-11", + matchPattern: "(?i)^(openai/)?(gpt-5.2-pro-2025-12-11)$", + startDate: "2025-12-12T09:00:06.513Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000021, + output: 0.000168, + output_reasoning_tokens: 0.000168, + output_reasoning: 0.000168, + }, + }, + ], + }, + { + modelName: "gpt-5.4", + matchPattern: "(?i)^(openai/)?(gpt-5.4)$", + startDate: "2026-03-05T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + input_cached_tokens: 2.5e-7, + input_cache_read: 2.5e-7, + output: 0.000015, + output_reasoning_tokens: 0.000015, + output_reasoning: 0.000015, + }, + }, + ], + }, + { + modelName: "gpt-5.4-pro", + matchPattern: "(?i)^(openai/)?(gpt-5.4-pro)$", + startDate: "2026-03-05T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00003, + output: 0.00018, + output_reasoning_tokens: 0.00018, + output_reasoning: 0.00018, + }, + }, + ], + }, + { + modelName: "gpt-5.4-2026-03-05", + matchPattern: "(?i)^(openai/)?(gpt-5.4-2026-03-05)$", + startDate: "2026-03-05T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + input_cached_tokens: 2.5e-7, + input_cache_read: 2.5e-7, + output: 0.000015, + output_reasoning_tokens: 0.000015, + output_reasoning: 0.000015, + }, + }, + ], + }, + { + modelName: "gpt-5.4-pro-2026-03-05", + matchPattern: "(?i)^(openai/)?(gpt-5.4-pro-2026-03-05)$", + startDate: "2026-03-05T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00003, + output: 0.00018, + output_reasoning_tokens: 0.00018, + output_reasoning: 0.00018, + }, + }, + ], + }, + { + modelName: "gpt-5.4-mini", + matchPattern: "(?i)^(openai\\/)?(gpt-5.4-mini)$", + startDate: "2026-03-18T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 7.5e-7, + input_cached_tokens: 7.5e-8, + input_cache_read: 7.5e-8, + output: 0.0000045, + }, + }, + ], + }, + { + modelName: "gpt-5.4-mini-2026-03-17", + matchPattern: "(?i)^(openai\\/)?(gpt-5.4-mini-2026-03-17)$", + startDate: "2026-03-18T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 7.5e-7, + input_cached_tokens: 7.5e-8, + input_cache_read: 7.5e-8, + output: 0.0000045, + }, + }, + ], + }, + { + modelName: "gpt-5.4-nano", + matchPattern: "(?i)^(openai\\/)?(gpt-5.4-nano)$", + startDate: "2026-03-18T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2e-7, + input_cached_tokens: 2e-8, + input_cache_read: 2e-8, + output: 0.00000125, + }, + }, + ], + }, + { + modelName: "gpt-5.4-nano-2026-03-17", + matchPattern: "(?i)^(openai\\/)?(gpt-5.4-nano-2026-03-17)$", + startDate: "2026-03-18T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2e-7, + input_cached_tokens: 2e-8, + input_cache_read: 2e-8, + output: 0.00000125, + }, + }, + ], + }, + { + modelName: "gemini-3-flash-preview", + matchPattern: "(?i)^(google(ai)?/)?(gemini-3-flash-preview)$", + startDate: "2025-12-21T12:01:42.282Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-7, + input_text: 5e-7, + input_modality_1: 5e-7, + prompt_token_count: 5e-7, + promptTokenCount: 5e-7, + input_cached_tokens: 5e-8, + cached_content_token_count: 5e-8, + output: 0.000003, + output_text: 0.000003, + output_modality_1: 0.000003, + candidates_token_count: 0.000003, + candidatesTokenCount: 0.000003, + thoughtsTokenCount: 0.000003, + thoughts_token_count: 0.000003, + output_reasoning: 0.000003, + }, + }, + ], + }, + { + modelName: "gemini-3.1-flash-lite-preview", + matchPattern: "(?i)^(google(ai)?/)?(gemini-3.1-flash-lite-preview)$", + startDate: "2026-03-03T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + input_modality_1: 2.5e-7, + input_text: 2.5e-7, + prompt_token_count: 2.5e-7, + promptTokenCount: 2.5e-7, + input_cached_tokens: 2.5e-8, + cached_content_token_count: 2.5e-8, + output: 0.0000015, + output_text: 0.0000015, + output_modality_1: 0.0000015, + candidates_token_count: 0.0000015, + candidatesTokenCount: 0.0000015, + thoughtsTokenCount: 0.0000015, + thoughts_token_count: 0.0000015, + output_reasoning: 0.0000015, + input_audio_tokens: 5e-7, + }, + }, + ], + }, + { + modelName: "gemini-live-2.5-flash-native-audio", + matchPattern: "(?i)^(google/)?(gemini-live-2.5-flash-native-audio)$", + startDate: "2026-03-16T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input_text: 5e-7, + input_audio: 0.000003, + input_image: 0.000003, + output_text: 0.000002, + output_audio: 0.000012, + }, + }, + ], + }, ]; diff --git a/internal-packages/llm-model-catalog/src/model-catalog.json b/internal-packages/llm-model-catalog/src/model-catalog.json index 0f60268352..23056f5fda 100644 --- a/internal-packages/llm-model-catalog/src/model-catalog.json +++ b/internal-packages/llm-model-catalog/src/model-catalog.json @@ -27,9 +27,7 @@ "description": "An early-generation Claude model from Anthropic, offering basic conversational and text completion capabilities. It was quickly superseded by Claude 1.2, 1.3, and the Claude 2 family.", "contextWindow": 9000, "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -44,9 +42,7 @@ "description": "An early-generation Anthropic model, part of the original Claude 1.x family. It offered improved performance over Claude 1.0 but was quickly superseded by Claude 1.3 and later model families.", "contextWindow": 9000, "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": null, "isHidden": true, "supportsStructuredOutput": false, @@ -61,9 +57,7 @@ "description": "Early-generation Claude model from Anthropic, offering improved performance over Claude 1.0-1.2 in reasoning and instruction-following tasks.", "contextWindow": 100000, "maxOutputTokens": null, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -78,9 +72,7 @@ "description": "Anthropic's second-generation large language model, offering improved performance over Claude 1.x with longer context support. Succeeded by Claude 2.1 and later the Claude 3 family.", "contextWindow": 100000, "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-07-11", "isHidden": true, "supportsStructuredOutput": false, @@ -95,10 +87,7 @@ "description": "Anthropic's Claude 2.1 model featuring a 200K context window, reduced hallucination rates compared to Claude 2.0, and improved accuracy on long document comprehension.", "contextWindow": 200000, "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "tool_use" - ], + "capabilities": ["streaming", "tool_use"], "releaseDate": "2023-11-21", "isHidden": true, "supportsStructuredOutput": false, @@ -113,12 +102,7 @@ "description": "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across common tasks.", "contextWindow": 200000, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-10-22", "isHidden": false, "supportsStructuredOutput": true, @@ -133,12 +117,7 @@ "description": "Anthropic's Claude 3.5 Sonnet is a mid-tier model balancing intelligence and speed, excelling at coding, analysis, and vision tasks while being faster and cheaper than Opus.", "contextWindow": 200000, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-06-20", "isHidden": true, "supportsStructuredOutput": true, @@ -153,12 +132,7 @@ "description": "Anthropic's fastest and most compact Claude 3 model, optimized for speed and cost-efficiency while maintaining strong performance on everyday tasks.", "contextWindow": 200000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-03-13", "isHidden": true, "supportsStructuredOutput": false, @@ -173,12 +147,7 @@ "description": "Anthropic's most capable model in the Claude 3 family, excelling at complex analysis, nuanced content generation, and advanced reasoning tasks.", "contextWindow": 200000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-03-04", "isHidden": true, "supportsStructuredOutput": true, @@ -193,12 +162,7 @@ "description": "Mid-tier model in Anthropic's Claude 3 family, balancing performance and speed for a wide range of tasks including analysis, coding, and content generation.", "contextWindow": 200000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-03-04", "isHidden": true, "supportsStructuredOutput": true, @@ -213,12 +177,7 @@ "description": "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across a wide range of tasks.", "contextWindow": 200000, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-10-29", "isHidden": false, "supportsStructuredOutput": true, @@ -233,12 +192,7 @@ "description": "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", "contextWindow": 200000, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-06-20", "isHidden": true, "supportsStructuredOutput": true, @@ -253,12 +207,7 @@ "description": "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", "contextWindow": 200000, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-06-20", "isHidden": true, "supportsStructuredOutput": true, @@ -273,13 +222,7 @@ "description": "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", "contextWindow": 200000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-02-24", "isHidden": true, "supportsStructuredOutput": true, @@ -294,13 +237,7 @@ "description": "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", "contextWindow": 200000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-02-24", "isHidden": true, "supportsStructuredOutput": true, @@ -315,13 +252,7 @@ "description": "Anthropic's fastest model with near-frontier intelligence, optimized for speed and cost efficiency while supporting extended thinking and vision.", "contextWindow": 200000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-10-01", "isHidden": false, "supportsStructuredOutput": true, @@ -336,9 +267,7 @@ "description": "Anthropic's fast and cost-effective model optimized for speed and efficiency, positioned as a lighter alternative to Claude 1.x for tasks requiring lower latency.", "contextWindow": 100000, "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -353,9 +282,7 @@ "description": "Anthropic's fast and cost-effective model, optimized for speed and efficiency while maintaining strong performance on conversational and text generation tasks.", "contextWindow": 100000, "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-08-09", "isHidden": true, "supportsStructuredOutput": false, @@ -370,13 +297,7 @@ "description": "Anthropic's hybrid reasoning model with strong software engineering and agentic capabilities, scoring 74.5% on SWE-bench Verified. Supports both rapid responses and step-by-step extended thinking.", "contextWindow": 200000, "maxOutputTokens": 32000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-08-05", "isHidden": true, "supportsStructuredOutput": true, @@ -391,13 +312,7 @@ "description": "Anthropic's flagship model from the Claude 4 family, excelling at complex coding tasks, long-running agent workflows, and deep reasoning with extended thinking support.", "contextWindow": 200000, "maxOutputTokens": 32000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-05-14", "isHidden": false, "supportsStructuredOutput": true, @@ -412,13 +327,7 @@ "description": "Anthropic's flagship intelligence model released in November 2025, excelling at complex reasoning, vision, and extended thinking with the best performance in Anthropic's lineup before Opus 4.6.", "contextWindow": 200000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-11-01", "isHidden": false, "supportsStructuredOutput": true, @@ -433,13 +342,7 @@ "description": "Anthropic's most intelligent model, optimized for building agents and coding with exceptional reasoning capabilities and extended agentic task horizons.", "contextWindow": 1000000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2026-02-05", "isHidden": false, "supportsStructuredOutput": true, @@ -454,13 +357,7 @@ "description": "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", "contextWindow": 200000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-05-14", "isHidden": true, "supportsStructuredOutput": true, @@ -475,13 +372,7 @@ "description": "Anthropic's high-performance mid-tier model with strong coding, reasoning, and multi-step problem solving capabilities. Successor to Claude Sonnet 4, offering improved benchmarks at the same price point.", "contextWindow": 200000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-09-29", "isHidden": false, "supportsStructuredOutput": true, @@ -496,13 +387,7 @@ "description": "Anthropic's best combination of speed and intelligence, excelling at coding, agentic tasks, and computer use, with a 1M token context window and performance rivaling prior Opus-class models.", "contextWindow": 1000000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2026-02-17", "isHidden": false, "supportsStructuredOutput": true, @@ -517,13 +402,7 @@ "description": "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", "contextWindow": 200000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-05-14", "isHidden": true, "supportsStructuredOutput": true, @@ -538,11 +417,7 @@ "description": "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", "contextWindow": 32760, "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-12-13", "isHidden": true, "supportsStructuredOutput": false, @@ -557,11 +432,7 @@ "description": "Google's first-generation Pro model optimized for text generation, reasoning, and multi-turn conversation tasks, part of the original Gemini 1.0 lineup.", "contextWindow": 30720, "maxOutputTokens": 2048, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2024-02-15", "isHidden": true, "supportsStructuredOutput": false, @@ -576,11 +447,7 @@ "description": "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", "contextWindow": 32760, "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-12-13", "isHidden": true, "supportsStructuredOutput": false, @@ -595,13 +462,7 @@ "description": "Google's mid-size multimodal model with a massive context window, strong at long-document understanding, code generation, and multi-turn conversation.", "contextWindow": 2097152, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "audio_input" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "audio_input"], "releaseDate": "2024-02-15", "isHidden": true, "supportsStructuredOutput": true, @@ -662,12 +523,7 @@ "description": "A lightweight, cost-efficient variant of Gemini 2.0 Flash optimized for low latency and high throughput, supporting multimodal input with text output.", "contextWindow": 1048576, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-02-05", "isHidden": true, "supportsStructuredOutput": true, @@ -682,12 +538,7 @@ "description": "Google's cost-optimized, low-latency model in the Gemini 2.0 family, designed for high-volume tasks like summarization, multimodal processing, and categorization.", "contextWindow": 1048576, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-02-05", "isHidden": true, "supportsStructuredOutput": true, @@ -863,11 +714,7 @@ "description": "Google's first-generation Gemini model for text generation, reasoning, and multi-turn conversation. Superseded by Gemini 1.5 Pro and later models.", "contextWindow": 32768, "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-12-13", "isHidden": true, "supportsStructuredOutput": false, @@ -882,12 +729,7 @@ "description": "OpenAI's fast and cost-effective model optimized for chat and instruction-following tasks, now superseded by GPT-4o mini.", "contextWindow": 16385, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2023-03-01", "isHidden": true, "supportsStructuredOutput": false, @@ -902,12 +744,7 @@ "description": "A fast and cost-effective GPT-3.5 Turbo snapshot optimized for chat completions, offering improved accuracy for function calling and reduced instances of incomplete responses.", "contextWindow": 16385, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2024-01-25", "isHidden": true, "supportsStructuredOutput": false, @@ -922,10 +759,7 @@ "description": "Early snapshot of GPT-3.5 Turbo, OpenAI's first ChatGPT-optimized model for chat completions. Fast and cost-effective for simple tasks but superseded by later revisions.", "contextWindow": 4096, "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "fine_tunable" - ], + "capabilities": ["streaming", "fine_tunable"], "releaseDate": "2023-03-01", "isHidden": true, "supportsStructuredOutput": false, @@ -940,12 +774,7 @@ "description": "A snapshot of GPT-3.5 Turbo from June 2023, optimized for chat and instruction-following tasks with function calling support.", "contextWindow": 4096, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2023-06-13", "isHidden": true, "supportsStructuredOutput": false, @@ -960,12 +789,7 @@ "description": "A dated snapshot of GPT-3.5 Turbo released in November 2023, offering improved instruction following, JSON mode, and parallel function calling over previous GPT-3.5 variants.", "contextWindow": 16385, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2023-11-06", "isHidden": true, "supportsStructuredOutput": false, @@ -980,12 +804,7 @@ "description": "Extended context version of GPT-3.5 Turbo with 16K token context window, offering the same capabilities as the base model but able to process longer inputs.", "contextWindow": 16384, "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "json_mode", - "fine_tunable", - "tool_use" - ], + "capabilities": ["streaming", "json_mode", "fine_tunable", "tool_use"], "releaseDate": "2023-06-13", "isHidden": true, "supportsStructuredOutput": false, @@ -1000,11 +819,7 @@ "description": "Extended context window variant of GPT-3.5 Turbo with 16K token context, snapshot from June 2023. Optimized for chat completions with longer document processing.", "contextWindow": 16384, "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["streaming", "json_mode", "fine_tunable"], "releaseDate": "2023-06-13", "isHidden": true, "supportsStructuredOutput": false, @@ -1019,9 +834,7 @@ "description": "OpenAI's GPT-3.5 Turbo Instruct is a completions-only model (not chat) optimized for following explicit instructions, replacing the legacy text-davinci-003 model.", "contextWindow": 4096, "maxOutputTokens": 4096, - "capabilities": [ - "fine_tunable" - ], + "capabilities": ["fine_tunable"], "releaseDate": "2023-09-19", "isHidden": true, "supportsStructuredOutput": false, @@ -1036,12 +849,7 @@ "description": "OpenAI's flagship large language model that preceded GPT-4o, known for strong reasoning and instruction-following capabilities across a wide range of tasks.", "contextWindow": 8192, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -1056,11 +864,7 @@ "description": "An improved GPT-4 Turbo preview model with better task completion, reduced laziness in code generation, and enhanced instruction following.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2024-01-25", "isHidden": true, "supportsStructuredOutput": false, @@ -1075,9 +879,7 @@ "description": "Original GPT-4 snapshot from March 2023, a large multimodal model (text-only at launch) that was one of OpenAI's first GPT-4 releases. Now deprecated and replaced by newer GPT-4 variants.", "contextWindow": 8192, "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -1092,12 +894,7 @@ "description": "A snapshot of GPT-4 from June 2023, offering strong reasoning and instruction-following capabilities. It was one of the first widely available GPT-4 variants with function calling support.", "contextWindow": 8192, "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2023-06-13", "isHidden": true, "supportsStructuredOutput": false, @@ -1112,12 +909,7 @@ "description": "GPT-4 Turbo preview model with 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2023-11-06", "isHidden": true, "supportsStructuredOutput": false, @@ -1132,11 +924,7 @@ "description": "Extended context window variant of GPT-4 with 32,768 token capacity, offering the same capabilities as GPT-4 but able to process longer documents and conversations.", "contextWindow": 32768, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -1151,9 +939,7 @@ "description": "Extended context (32k token) variant of the original GPT-4 launch snapshot from March 2024, offering the same capabilities as gpt-4-0314 but with 4x the context window.", "contextWindow": 32768, "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -1168,11 +954,7 @@ "description": "Extended context window variant of GPT-4 with 32,768 token context, based on the June 2023 snapshot. Offers the same capabilities as GPT-4 but with 4x the context length.", "contextWindow": 32768, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-06-13", "isHidden": true, "supportsStructuredOutput": false, @@ -1187,11 +969,7 @@ "description": "GPT-4 Turbo preview model with 128K context window, JSON mode, and parallel function calling. A preview release in the GPT-4 Turbo series, now deprecated in favor of newer models.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-11-06", "isHidden": true, "supportsStructuredOutput": false, @@ -1206,12 +984,7 @@ "description": "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-04-09", "isHidden": false, "supportsStructuredOutput": false, @@ -1226,12 +999,7 @@ "description": "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-04-09", "isHidden": false, "supportsStructuredOutput": false, @@ -1246,12 +1014,7 @@ "description": "An early preview of GPT-4 Turbo with a 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-01-25", "isHidden": true, "supportsStructuredOutput": false, @@ -1266,12 +1029,7 @@ "description": "OpenAI's GPT-4 Turbo model with vision capabilities, able to analyze and understand images alongside text. It was a preview model later superseded by GPT-4 Turbo (gpt-4-turbo-2024-04-09) and then GPT-4o.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2023-11-06", "isHidden": true, "supportsStructuredOutput": false, @@ -1286,12 +1044,7 @@ "description": "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", "contextWindow": 1047576, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1306,12 +1059,7 @@ "description": "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", "contextWindow": 1047576, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1326,13 +1074,7 @@ "description": "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", "contextWindow": 1000000, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1347,13 +1089,7 @@ "description": "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", "contextWindow": 1000000, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1368,12 +1104,7 @@ "description": "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", "contextWindow": 1047576, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1388,12 +1119,7 @@ "description": "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", "contextWindow": 1047576, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1408,12 +1134,7 @@ "description": "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-02-27", "isHidden": true, "supportsStructuredOutput": true, @@ -1428,12 +1149,7 @@ "description": "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-02-27", "isHidden": true, "supportsStructuredOutput": true, @@ -1540,12 +1256,7 @@ "description": "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], + "capabilities": ["audio_input", "audio_output", "tool_use", "streaming"], "releaseDate": "2024-10-01", "isHidden": false, "supportsStructuredOutput": false, @@ -1560,12 +1271,7 @@ "description": "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], + "capabilities": ["audio_input", "audio_output", "tool_use", "streaming"], "releaseDate": "2024-10-01", "isHidden": false, "supportsStructuredOutput": false, @@ -1580,13 +1286,7 @@ "description": "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2024-07-18", "isHidden": false, "supportsStructuredOutput": true, @@ -1601,13 +1301,7 @@ "description": "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2024-07-18", "isHidden": false, "supportsStructuredOutput": true, @@ -1622,12 +1316,7 @@ "description": "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], + "capabilities": ["audio_input", "audio_output", "tool_use", "streaming"], "releaseDate": "2024-10-01", "isHidden": false, "supportsStructuredOutput": false, @@ -1642,12 +1331,7 @@ "description": "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], + "capabilities": ["audio_input", "audio_output", "tool_use", "streaming"], "releaseDate": "2024-10-01", "isHidden": false, "supportsStructuredOutput": false, @@ -1706,12 +1390,7 @@ "description": "Non-reasoning GPT-5 model used in ChatGPT, optimized for conversational tasks. Supports text and image inputs with function calling and structured outputs.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-08-07", "isHidden": false, "supportsStructuredOutput": true, @@ -1726,12 +1405,7 @@ "description": "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-08-07", "isHidden": false, "supportsStructuredOutput": true, @@ -1746,12 +1420,7 @@ "description": "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-08-07", "isHidden": false, "supportsStructuredOutput": true, @@ -1766,12 +1435,7 @@ "description": "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-08-07", "isHidden": false, "supportsStructuredOutput": true, @@ -1786,12 +1450,7 @@ "description": "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-08-07", "isHidden": false, "supportsStructuredOutput": true, @@ -1806,12 +1465,7 @@ "description": "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-10-06", "isHidden": false, "supportsStructuredOutput": true, @@ -1826,12 +1480,7 @@ "description": "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-10-06", "isHidden": false, "supportsStructuredOutput": true, @@ -1846,12 +1495,7 @@ "description": "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-11-13", "isHidden": false, "supportsStructuredOutput": true, @@ -1866,12 +1510,7 @@ "description": "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-11-13", "isHidden": false, "supportsStructuredOutput": true, @@ -1886,13 +1525,7 @@ "description": "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-12-11", "isHidden": false, "supportsStructuredOutput": true, @@ -1907,13 +1540,7 @@ "description": "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-12-11", "isHidden": false, "supportsStructuredOutput": true, @@ -1928,12 +1555,7 @@ "description": "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "extended_thinking"], "releaseDate": "2025-12-11", "isHidden": false, "supportsStructuredOutput": false, @@ -1948,12 +1570,7 @@ "description": "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "extended_thinking"], "releaseDate": "2025-12-11", "isHidden": false, "supportsStructuredOutput": false, @@ -1968,13 +1585,7 @@ "description": "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", "contextWindow": 1050000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "code_execution" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "code_execution"], "releaseDate": "2026-03-05", "isHidden": false, "supportsStructuredOutput": true, @@ -1989,13 +1600,7 @@ "description": "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", "contextWindow": 1050000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "code_execution" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "code_execution"], "releaseDate": "2026-03-05", "isHidden": false, "supportsStructuredOutput": true, @@ -2010,13 +1615,7 @@ "description": "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2026-03-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2031,13 +1630,7 @@ "description": "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2026-03-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2052,12 +1645,7 @@ "description": "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2026-03-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2072,12 +1660,7 @@ "description": "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2026-03-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2092,12 +1675,7 @@ "description": "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", "contextWindow": 1050000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "extended_thinking"], "releaseDate": "2026-03-05", "isHidden": false, "supportsStructuredOutput": false, @@ -2112,12 +1690,7 @@ "description": "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", "contextWindow": 1050000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "extended_thinking"], "releaseDate": "2026-03-05", "isHidden": false, "supportsStructuredOutput": false, @@ -2132,12 +1705,7 @@ "description": "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-12-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2152,12 +1720,7 @@ "description": "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-12-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2172,10 +1735,7 @@ "description": "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", "contextWindow": 128000, "maxOutputTokens": 65536, - "capabilities": [ - "streaming", - "json_mode" - ], + "capabilities": ["streaming", "json_mode"], "releaseDate": "2024-09-12", "isHidden": true, "supportsStructuredOutput": false, @@ -2190,10 +1750,7 @@ "description": "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", "contextWindow": 128000, "maxOutputTokens": 65536, - "capabilities": [ - "streaming", - "json_mode" - ], + "capabilities": ["streaming", "json_mode"], "releaseDate": "2024-09-12", "isHidden": true, "supportsStructuredOutput": false, @@ -2208,9 +1765,7 @@ "description": "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", "contextWindow": 128000, "maxOutputTokens": 32768, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2024-09-12", "isHidden": true, "supportsStructuredOutput": false, @@ -2225,9 +1780,7 @@ "description": "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", "contextWindow": 128000, "maxOutputTokens": 32768, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2024-09-12", "isHidden": true, "supportsStructuredOutput": false, @@ -2242,12 +1795,7 @@ "description": "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "json_mode", "extended_thinking"], "releaseDate": "2025-03-19", "isHidden": false, "supportsStructuredOutput": true, @@ -2262,12 +1810,7 @@ "description": "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "json_mode", "extended_thinking"], "releaseDate": "2025-03-19", "isHidden": false, "supportsStructuredOutput": true, @@ -2282,13 +1825,7 @@ "description": "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-04-16", "isHidden": false, "supportsStructuredOutput": true, @@ -2303,13 +1840,7 @@ "description": "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-04-16", "isHidden": false, "supportsStructuredOutput": true, @@ -2324,12 +1855,7 @@ "description": "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-01-31", "isHidden": false, "supportsStructuredOutput": true, @@ -2344,12 +1870,7 @@ "description": "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-01-31", "isHidden": false, "supportsStructuredOutput": true, @@ -2364,13 +1885,7 @@ "description": "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-06-10", "isHidden": false, "supportsStructuredOutput": true, @@ -2385,13 +1900,7 @@ "description": "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-06-10", "isHidden": false, "supportsStructuredOutput": true, @@ -2406,13 +1915,7 @@ "description": "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-04-16", "isHidden": false, "supportsStructuredOutput": true, @@ -2427,13 +1930,7 @@ "description": "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-04-16", "isHidden": false, "supportsStructuredOutput": true, diff --git a/internal-packages/llm-model-catalog/src/modelCatalog.ts b/internal-packages/llm-model-catalog/src/modelCatalog.ts index 71ae921c3e..d90eb1a992 100644 --- a/internal-packages/llm-model-catalog/src/modelCatalog.ts +++ b/internal-packages/llm-model-catalog/src/modelCatalog.ts @@ -5,677 +5,564 @@ import type { ModelCatalogEntry } from "./types.js"; export const modelCatalog: Record = { "chatgpt-4o-latest": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model optimized for speed and cost, capable of processing text, images, and audio with strong performance across reasoning, coding, and creative tasks.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship multimodal model optimized for speed and cost, capable of processing text, images, and audio with strong performance across reasoning, coding, and creative tasks.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable" + "fine_tunable", ], - "releaseDate": "2024-05-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T10:55:46.469Z", - "baseModelName": "chatgpt-4o" + releaseDate: "2024-05-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T10:55:46.469Z", + baseModelName: "chatgpt-4o", }, "claude-1.1": { - "provider": "anthropic", - "description": "An early-generation Claude model from Anthropic, offering basic conversational and text completion capabilities. It was quickly superseded by Claude 1.2, 1.3, and the Claude 2 family.", - "contextWindow": 9000, - "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": null, - "resolvedAt": "2026-03-24T10:55:47.906Z", - "baseModelName": null + provider: "anthropic", + description: + "An early-generation Claude model from Anthropic, offering basic conversational and text completion capabilities. It was quickly superseded by Claude 1.2, 1.3, and the Claude 2 family.", + contextWindow: 9000, + maxOutputTokens: 8191, + capabilities: ["streaming"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: null, + resolvedAt: "2026-03-24T10:55:47.906Z", + baseModelName: null, }, "claude-1.2": { - "provider": "anthropic", - "description": "An early-generation Anthropic model, part of the original Claude 1.x family. It offered improved performance over Claude 1.0 but was quickly superseded by Claude 1.3 and later model families.", - "contextWindow": 9000, - "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], - "releaseDate": null, - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": null, - "resolvedAt": "2026-03-24T10:55:46.760Z", - "baseModelName": null + provider: "anthropic", + description: + "An early-generation Anthropic model, part of the original Claude 1.x family. It offered improved performance over Claude 1.0 but was quickly superseded by Claude 1.3 and later model families.", + contextWindow: 9000, + maxOutputTokens: 8191, + capabilities: ["streaming"], + releaseDate: null, + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: null, + resolvedAt: "2026-03-24T10:55:46.760Z", + baseModelName: null, }, "claude-1.3": { - "provider": "anthropic", - "description": "Early-generation Claude model from Anthropic, offering improved performance over Claude 1.0-1.2 in reasoning and instruction-following tasks.", - "contextWindow": 100000, - "maxOutputTokens": null, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": null, - "resolvedAt": "2026-03-24T10:55:46.227Z", - "baseModelName": null + provider: "anthropic", + description: + "Early-generation Claude model from Anthropic, offering improved performance over Claude 1.0-1.2 in reasoning and instruction-following tasks.", + contextWindow: 100000, + maxOutputTokens: null, + capabilities: ["streaming"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: null, + resolvedAt: "2026-03-24T10:55:46.227Z", + baseModelName: null, }, "claude-2.0": { - "provider": "anthropic", - "description": "Anthropic's second-generation large language model, offering improved performance over Claude 1.x with longer context support. Succeeded by Claude 2.1 and later the Claude 3 family.", - "contextWindow": 100000, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-07-11", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2023-02-01", - "resolvedAt": "2026-03-24T10:55:45.922Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's second-generation large language model, offering improved performance over Claude 1.x with longer context support. Succeeded by Claude 2.1 and later the Claude 3 family.", + contextWindow: 100000, + maxOutputTokens: 4096, + capabilities: ["streaming"], + releaseDate: "2023-07-11", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2023-02-01", + resolvedAt: "2026-03-24T10:55:45.922Z", + baseModelName: null, }, "claude-2.1": { - "provider": "anthropic", - "description": "Anthropic's Claude 2.1 model featuring a 200K context window, reduced hallucination rates compared to Claude 2.0, and improved accuracy on long document comprehension.", - "contextWindow": 200000, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "tool_use" - ], - "releaseDate": "2023-11-21", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2023-01-01", - "resolvedAt": "2026-03-24T10:56:22.743Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's Claude 2.1 model featuring a 200K context window, reduced hallucination rates compared to Claude 2.0, and improved accuracy on long document comprehension.", + contextWindow: 200000, + maxOutputTokens: 4096, + capabilities: ["streaming", "tool_use"], + releaseDate: "2023-11-21", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2023-01-01", + resolvedAt: "2026-03-24T10:56:22.743Z", + baseModelName: null, }, "claude-3-5-haiku-20241022": { - "provider": "anthropic", - "description": "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across common tasks.", - "contextWindow": 200000, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-10-22", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-07-01", - "resolvedAt": "2026-03-24T10:56:25.724Z", - "baseModelName": "claude-3-5-haiku" + provider: "anthropic", + description: + "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across common tasks.", + contextWindow: 200000, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-10-22", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-07-01", + resolvedAt: "2026-03-24T10:56:25.724Z", + baseModelName: "claude-3-5-haiku", }, "claude-3-5-sonnet-20240620": { - "provider": "anthropic", - "description": "Anthropic's Claude 3.5 Sonnet is a mid-tier model balancing intelligence and speed, excelling at coding, analysis, and vision tasks while being faster and cheaper than Opus.", - "contextWindow": 200000, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-06-20", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-04-01", - "resolvedAt": "2026-03-24T10:56:35.401Z", - "baseModelName": "claude-3-5-sonnet" + provider: "anthropic", + description: + "Anthropic's Claude 3.5 Sonnet is a mid-tier model balancing intelligence and speed, excelling at coding, analysis, and vision tasks while being faster and cheaper than Opus.", + contextWindow: 200000, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-06-20", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-04-01", + resolvedAt: "2026-03-24T10:56:35.401Z", + baseModelName: "claude-3-5-sonnet", }, "claude-3-haiku-20240307": { - "provider": "anthropic", - "description": "Anthropic's fastest and most compact Claude 3 model, optimized for speed and cost-efficiency while maintaining strong performance on everyday tasks.", - "contextWindow": 200000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-03-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-08-01", - "resolvedAt": "2026-03-24T10:56:25.288Z", - "baseModelName": "claude-3-haiku" + provider: "anthropic", + description: + "Anthropic's fastest and most compact Claude 3 model, optimized for speed and cost-efficiency while maintaining strong performance on everyday tasks.", + contextWindow: 200000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-03-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-08-01", + resolvedAt: "2026-03-24T10:56:25.288Z", + baseModelName: "claude-3-haiku", }, "claude-3-opus-20240229": { - "provider": "anthropic", - "description": "Anthropic's most capable model in the Claude 3 family, excelling at complex analysis, nuanced content generation, and advanced reasoning tasks.", - "contextWindow": 200000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-03-04", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-08-01", - "resolvedAt": "2026-03-24T10:56:26.008Z", - "baseModelName": "claude-3-opus" + provider: "anthropic", + description: + "Anthropic's most capable model in the Claude 3 family, excelling at complex analysis, nuanced content generation, and advanced reasoning tasks.", + contextWindow: 200000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-03-04", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-08-01", + resolvedAt: "2026-03-24T10:56:26.008Z", + baseModelName: "claude-3-opus", }, "claude-3-sonnet-20240229": { - "provider": "anthropic", - "description": "Mid-tier model in Anthropic's Claude 3 family, balancing performance and speed for a wide range of tasks including analysis, coding, and content generation.", - "contextWindow": 200000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-03-04", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-02-01", - "resolvedAt": "2026-03-24T10:56:59.532Z", - "baseModelName": "claude-3-sonnet" + provider: "anthropic", + description: + "Mid-tier model in Anthropic's Claude 3 family, balancing performance and speed for a wide range of tasks including analysis, coding, and content generation.", + contextWindow: 200000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-03-04", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-02-01", + resolvedAt: "2026-03-24T10:56:59.532Z", + baseModelName: "claude-3-sonnet", }, "claude-3.5-haiku-latest": { - "provider": "anthropic", - "description": "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across a wide range of tasks.", - "contextWindow": 200000, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-10-29", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-07-01", - "resolvedAt": "2026-03-24T10:57:04.392Z", - "baseModelName": "claude-3.5-haiku" + provider: "anthropic", + description: + "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across a wide range of tasks.", + contextWindow: 200000, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-10-29", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-07-01", + resolvedAt: "2026-03-24T10:57:04.392Z", + baseModelName: "claude-3.5-haiku", }, "claude-3.5-sonnet-20241022": { - "provider": "anthropic", - "description": "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", - "contextWindow": 200000, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-06-20", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-04-01", - "resolvedAt": "2026-03-24T10:57:13.346Z", - "baseModelName": "claude-3.5-sonnet" + provider: "anthropic", + description: + "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", + contextWindow: 200000, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-06-20", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-04-01", + resolvedAt: "2026-03-24T10:57:13.346Z", + baseModelName: "claude-3.5-sonnet", }, "claude-3.5-sonnet-latest": { - "provider": "anthropic", - "description": "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", - "contextWindow": 200000, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-06-20", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-04-01", - "resolvedAt": "2026-03-24T10:57:13.346Z", - "baseModelName": "claude-3.5-sonnet" + provider: "anthropic", + description: + "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", + contextWindow: 200000, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-06-20", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-04-01", + resolvedAt: "2026-03-24T10:57:13.346Z", + baseModelName: "claude-3.5-sonnet", }, "claude-3.7-sonnet-20250219": { - "provider": "anthropic", - "description": "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", - "contextWindow": 200000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-02-24", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-04-01", - "resolvedAt": "2026-03-24T10:57:12.967Z", - "baseModelName": "claude-3.7-sonnet" + provider: "anthropic", + description: + "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", + contextWindow: 200000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-02-24", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-04-01", + resolvedAt: "2026-03-24T10:57:12.967Z", + baseModelName: "claude-3.7-sonnet", }, "claude-3.7-sonnet-latest": { - "provider": "anthropic", - "description": "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", - "contextWindow": 200000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-02-24", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-04-01", - "resolvedAt": "2026-03-24T10:57:12.967Z", - "baseModelName": "claude-3.7-sonnet" + provider: "anthropic", + description: + "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", + contextWindow: 200000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-02-24", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-04-01", + resolvedAt: "2026-03-24T10:57:12.967Z", + baseModelName: "claude-3.7-sonnet", }, "claude-haiku-4-5-20251001": { - "provider": "anthropic", - "description": "Anthropic's fastest model with near-frontier intelligence, optimized for speed and cost efficiency while supporting extended thinking and vision.", - "contextWindow": 200000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-10-01", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-07-01", - "resolvedAt": "2026-03-24T10:57:29.685Z", - "baseModelName": "claude-haiku-4-5" + provider: "anthropic", + description: + "Anthropic's fastest model with near-frontier intelligence, optimized for speed and cost efficiency while supporting extended thinking and vision.", + contextWindow: 200000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-10-01", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-07-01", + resolvedAt: "2026-03-24T10:57:29.685Z", + baseModelName: "claude-haiku-4-5", }, "claude-instant-1": { - "provider": "anthropic", - "description": "Anthropic's fast and cost-effective model optimized for speed and efficiency, positioned as a lighter alternative to Claude 1.x for tasks requiring lower latency.", - "contextWindow": 100000, - "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-01-06", - "knowledgeCutoff": "2023-01-01", - "resolvedAt": "2026-03-24T10:57:36.888Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's fast and cost-effective model optimized for speed and efficiency, positioned as a lighter alternative to Claude 1.x for tasks requiring lower latency.", + contextWindow: 100000, + maxOutputTokens: 8191, + capabilities: ["streaming"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-01-06", + knowledgeCutoff: "2023-01-01", + resolvedAt: "2026-03-24T10:57:36.888Z", + baseModelName: null, }, "claude-instant-1.2": { - "provider": "anthropic", - "description": "Anthropic's fast and cost-effective model, optimized for speed and efficiency while maintaining strong performance on conversational and text generation tasks.", - "contextWindow": 100000, - "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-08-09", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2023-01-01", - "resolvedAt": "2026-03-24T10:57:41.865Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's fast and cost-effective model, optimized for speed and efficiency while maintaining strong performance on conversational and text generation tasks.", + contextWindow: 100000, + maxOutputTokens: 8191, + capabilities: ["streaming"], + releaseDate: "2023-08-09", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2023-01-01", + resolvedAt: "2026-03-24T10:57:41.865Z", + baseModelName: null, }, "claude-opus-4-1-20250805": { - "provider": "anthropic", - "description": "Anthropic's hybrid reasoning model with strong software engineering and agentic capabilities, scoring 74.5% on SWE-bench Verified. Supports both rapid responses and step-by-step extended thinking.", - "contextWindow": 200000, - "maxOutputTokens": 32000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-08-05", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-03-01", - "resolvedAt": "2026-03-24T10:58:36.876Z", - "baseModelName": "claude-opus-4-1" + provider: "anthropic", + description: + "Anthropic's hybrid reasoning model with strong software engineering and agentic capabilities, scoring 74.5% on SWE-bench Verified. Supports both rapid responses and step-by-step extended thinking.", + contextWindow: 200000, + maxOutputTokens: 32000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-08-05", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-03-01", + resolvedAt: "2026-03-24T10:58:36.876Z", + baseModelName: "claude-opus-4-1", }, "claude-opus-4-20250514": { - "provider": "anthropic", - "description": "Anthropic's flagship model from the Claude 4 family, excelling at complex coding tasks, long-running agent workflows, and deep reasoning with extended thinking support.", - "contextWindow": 200000, - "maxOutputTokens": 32000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-05-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-03-01", - "resolvedAt": "2026-03-24T10:58:47.518Z", - "baseModelName": "claude-opus-4" + provider: "anthropic", + description: + "Anthropic's flagship model from the Claude 4 family, excelling at complex coding tasks, long-running agent workflows, and deep reasoning with extended thinking support.", + contextWindow: 200000, + maxOutputTokens: 32000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-05-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-03-01", + resolvedAt: "2026-03-24T10:58:47.518Z", + baseModelName: "claude-opus-4", }, "claude-opus-4-5-20251101": { - "provider": "anthropic", - "description": "Anthropic's flagship intelligence model released in November 2025, excelling at complex reasoning, vision, and extended thinking with the best performance in Anthropic's lineup before Opus 4.6.", - "contextWindow": 200000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-11-01", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-03-01", - "resolvedAt": "2026-03-24T10:58:48.961Z", - "baseModelName": "claude-opus-4-5" + provider: "anthropic", + description: + "Anthropic's flagship intelligence model released in November 2025, excelling at complex reasoning, vision, and extended thinking with the best performance in Anthropic's lineup before Opus 4.6.", + contextWindow: 200000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-11-01", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-03-01", + resolvedAt: "2026-03-24T10:58:48.961Z", + baseModelName: "claude-opus-4-5", }, "claude-opus-4-6": { - "provider": "anthropic", - "description": "Anthropic's most intelligent model, optimized for building agents and coding with exceptional reasoning capabilities and extended agentic task horizons.", - "contextWindow": 1000000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2026-02-05", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-05-01", - "resolvedAt": "2026-03-24T10:58:42.061Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's most intelligent model, optimized for building agents and coding with exceptional reasoning capabilities and extended agentic task horizons.", + contextWindow: 1000000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2026-02-05", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-05-01", + resolvedAt: "2026-03-24T10:58:42.061Z", + baseModelName: null, }, "claude-sonnet-4-20250514": { - "provider": "anthropic", - "description": "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", - "contextWindow": 200000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-05-14", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-03-01", - "resolvedAt": "2026-03-24T10:58:39.601Z", - "baseModelName": "claude-sonnet-4" + provider: "anthropic", + description: + "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", + contextWindow: 200000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-05-14", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-03-01", + resolvedAt: "2026-03-24T10:58:39.601Z", + baseModelName: "claude-sonnet-4", }, "claude-sonnet-4-5-20250929": { - "provider": "anthropic", - "description": "Anthropic's high-performance mid-tier model with strong coding, reasoning, and multi-step problem solving capabilities. Successor to Claude Sonnet 4, offering improved benchmarks at the same price point.", - "contextWindow": 200000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-09-29", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T10:59:54.426Z", - "baseModelName": "claude-sonnet-4-5" + provider: "anthropic", + description: + "Anthropic's high-performance mid-tier model with strong coding, reasoning, and multi-step problem solving capabilities. Successor to Claude Sonnet 4, offering improved benchmarks at the same price point.", + contextWindow: 200000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-09-29", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T10:59:54.426Z", + baseModelName: "claude-sonnet-4-5", }, "claude-sonnet-4-6": { - "provider": "anthropic", - "description": "Anthropic's best combination of speed and intelligence, excelling at coding, agentic tasks, and computer use, with a 1M token context window and performance rivaling prior Opus-class models.", - "contextWindow": 1000000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2026-02-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2026-01-01", - "resolvedAt": "2026-03-24T10:59:59.014Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's best combination of speed and intelligence, excelling at coding, agentic tasks, and computer use, with a 1M token context window and performance rivaling prior Opus-class models.", + contextWindow: 1000000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2026-02-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2026-01-01", + resolvedAt: "2026-03-24T10:59:59.014Z", + baseModelName: null, }, "claude-sonnet-4-latest": { - "provider": "anthropic", - "description": "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", - "contextWindow": 200000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-05-14", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-03-01", - "resolvedAt": "2026-03-24T10:58:39.601Z", - "baseModelName": "claude-sonnet-4" + provider: "anthropic", + description: + "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", + contextWindow: 200000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-05-14", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-03-01", + resolvedAt: "2026-03-24T10:58:39.601Z", + baseModelName: "claude-sonnet-4", }, "gemini-1.0-pro": { - "provider": "google", - "description": "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", - "contextWindow": 32760, - "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-12-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-02-15", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T10:59:26.767Z", - "baseModelName": null + provider: "google", + description: + "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", + contextWindow: 32760, + maxOutputTokens: 8192, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-12-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-02-15", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T10:59:26.767Z", + baseModelName: null, }, "gemini-1.0-pro-001": { - "provider": "google", - "description": "Google's first-generation Pro model optimized for text generation, reasoning, and multi-turn conversation tasks, part of the original Gemini 1.0 lineup.", - "contextWindow": 30720, - "maxOutputTokens": 2048, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-02-15", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-02-15", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T10:59:27.391Z", - "baseModelName": null + provider: "google", + description: + "Google's first-generation Pro model optimized for text generation, reasoning, and multi-turn conversation tasks, part of the original Gemini 1.0 lineup.", + contextWindow: 30720, + maxOutputTokens: 2048, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2024-02-15", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-02-15", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T10:59:27.391Z", + baseModelName: null, }, "gemini-1.0-pro-latest": { - "provider": "google", - "description": "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", - "contextWindow": 32760, - "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-12-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-02-15", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T10:59:26.767Z", - "baseModelName": "gemini-1.0-pro" + provider: "google", + description: + "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", + contextWindow: 32760, + maxOutputTokens: 8192, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-12-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-02-15", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T10:59:26.767Z", + baseModelName: "gemini-1.0-pro", }, "gemini-1.5-pro-latest": { - "provider": "google", - "description": "Google's mid-size multimodal model with a massive context window, strong at long-document understanding, code generation, and multi-turn conversation.", - "contextWindow": 2097152, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "audio_input" - ], - "releaseDate": "2024-02-15", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-09-24", - "knowledgeCutoff": "2024-04-01", - "resolvedAt": "2026-03-24T10:59:25.463Z", - "baseModelName": "gemini-1.5-pro" + provider: "google", + description: + "Google's mid-size multimodal model with a massive context window, strong at long-document understanding, code generation, and multi-turn conversation.", + contextWindow: 2097152, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "audio_input"], + releaseDate: "2024-02-15", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-09-24", + knowledgeCutoff: "2024-04-01", + resolvedAt: "2026-03-24T10:59:25.463Z", + baseModelName: "gemini-1.5-pro", }, "gemini-2.0-flash": { - "provider": "google", - "description": "Google's second-generation workhorse model optimized for speed, with native tool use, multimodal input (text, images, audio, video), and a 1M token context window.", - "contextWindow": 1048576, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "code_execution", - "audio_input" - ], - "releaseDate": "2025-02-05", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-06-01", - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:01:15.429Z", - "baseModelName": null + provider: "google", + description: + "Google's second-generation workhorse model optimized for speed, with native tool use, multimodal input (text, images, audio, video), and a 1M token context window.", + contextWindow: 1048576, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "code_execution", "audio_input"], + releaseDate: "2025-02-05", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-06-01", + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:01:15.429Z", + baseModelName: null, }, "gemini-2.0-flash-001": { - "provider": "google", - "description": "Google's fast and efficient multimodal model that outperforms Gemini 1.5 Pro on key benchmarks at twice the speed, supporting text, image, audio, and video inputs with native tool use.", - "contextWindow": 1048576, - "maxOutputTokens": 8192, - "capabilities": [ + provider: "google", + description: + "Google's fast and efficient multimodal model that outperforms Gemini 1.5 Pro on key benchmarks at twice the speed, supporting text, image, audio, and video inputs with native tool use.", + contextWindow: 1048576, + maxOutputTokens: 8192, + capabilities: [ "vision", "tool_use", "streaming", @@ -683,1890 +570,1614 @@ export const modelCatalog: Record = { "audio_input", "image_generation", "audio_output", - "code_execution" + "code_execution", ], - "releaseDate": "2025-02-05", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-06-01", - "knowledgeCutoff": "2024-08-01", - "resolvedAt": "2026-03-24T11:01:04.084Z", - "baseModelName": null + releaseDate: "2025-02-05", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-06-01", + knowledgeCutoff: "2024-08-01", + resolvedAt: "2026-03-24T11:01:04.084Z", + baseModelName: null, }, "gemini-2.0-flash-lite-preview": { - "provider": "google", - "description": "A lightweight, cost-efficient variant of Gemini 2.0 Flash optimized for low latency and high throughput, supporting multimodal input with text output.", - "contextWindow": 1048576, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-02-05", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-06-01", - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:00:56.775Z", - "baseModelName": null + provider: "google", + description: + "A lightweight, cost-efficient variant of Gemini 2.0 Flash optimized for low latency and high throughput, supporting multimodal input with text output.", + contextWindow: 1048576, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-02-05", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-06-01", + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:00:56.775Z", + baseModelName: null, }, "gemini-2.0-flash-lite-preview-02-05": { - "provider": "google", - "description": "Google's cost-optimized, low-latency model in the Gemini 2.0 family, designed for high-volume tasks like summarization, multimodal processing, and categorization.", - "contextWindow": 1048576, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-02-05", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-12-09", - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:01:34.165Z", - "baseModelName": null + provider: "google", + description: + "Google's cost-optimized, low-latency model in the Gemini 2.0 family, designed for high-volume tasks like summarization, multimodal processing, and categorization.", + contextWindow: 1048576, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-02-05", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-12-09", + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:01:34.165Z", + baseModelName: null, }, "gemini-2.5-flash": { - "provider": "google", - "description": "Google's best price-performance model optimized for low-latency, high-volume tasks requiring reasoning, with built-in thinking capabilities and multimodal input support.", - "contextWindow": 1048576, - "maxOutputTokens": 65536, - "capabilities": [ + provider: "google", + description: + "Google's best price-performance model optimized for low-latency, high-volume tasks requiring reasoning, with built-in thinking capabilities and multimodal input support.", + contextWindow: 1048576, + maxOutputTokens: 65536, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2025-06-01", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:01:25.200Z", - "baseModelName": null + releaseDate: "2025-06-01", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:01:25.200Z", + baseModelName: null, }, "gemini-2.5-flash-lite": { - "provider": "google", - "description": "Google's most cost-efficient Gemini model, optimized for low-latency use cases with strong reasoning, multilingual, and long-context capabilities at minimal cost.", - "contextWindow": 1048576, - "maxOutputTokens": 65535, - "capabilities": [ + provider: "google", + description: + "Google's most cost-efficient Gemini model, optimized for low-latency use cases with strong reasoning, multilingual, and long-context capabilities at minimal cost.", + contextWindow: 1048576, + maxOutputTokens: 65535, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2025-07-22", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-07-22", - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:02:30.060Z", - "baseModelName": null + releaseDate: "2025-07-22", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-07-22", + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:02:30.060Z", + baseModelName: null, }, "gemini-2.5-pro": { - "provider": "google", - "description": "Google's most advanced reasoning model with deep thinking capabilities, excelling at complex tasks like coding, math, and multimodal understanding across text, images, audio, and video.", - "contextWindow": 1048576, - "maxOutputTokens": 65535, - "capabilities": [ + provider: "google", + description: + "Google's most advanced reasoning model with deep thinking capabilities, excelling at complex tasks like coding, math, and multimodal understanding across text, images, audio, and video.", + contextWindow: 1048576, + maxOutputTokens: 65535, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2025-03-25", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-06-17", - "knowledgeCutoff": "2025-01-31", - "resolvedAt": "2026-03-24T11:02:25.573Z", - "baseModelName": null + releaseDate: "2025-03-25", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-06-17", + knowledgeCutoff: "2025-01-31", + resolvedAt: "2026-03-24T11:02:25.573Z", + baseModelName: null, }, "gemini-3-flash-preview": { - "provider": "google", - "description": "Google's high-speed thinking model that matches Gemini 2.5 Pro performance at ~3x faster speed and lower cost, designed for agentic workflows, multi-turn chat, and coding assistance with configurable reasoning levels.", - "contextWindow": 1048576, - "maxOutputTokens": 65536, - "capabilities": [ + provider: "google", + description: + "Google's high-speed thinking model that matches Gemini 2.5 Pro performance at ~3x faster speed and lower cost, designed for agentic workflows, multi-turn chat, and coding assistance with configurable reasoning levels.", + contextWindow: 1048576, + maxOutputTokens: 65536, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2025-12-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:02:13.388Z", - "baseModelName": null + releaseDate: "2025-12-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:02:13.388Z", + baseModelName: null, }, "gemini-3-pro-preview": { - "provider": "google", - "description": "Google's flagship reasoning and multimodal model with strong coding and agentic capabilities, now deprecated in favor of Gemini 3.1 Pro.", - "contextWindow": 1048576, - "maxOutputTokens": 65536, - "capabilities": [ + provider: "google", + description: + "Google's flagship reasoning and multimodal model with strong coding and agentic capabilities, now deprecated in favor of Gemini 3.1 Pro.", + contextWindow: 1048576, + maxOutputTokens: 65536, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2025-11-01", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-03-09", - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:02:29.313Z", - "baseModelName": null + releaseDate: "2025-11-01", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-03-09", + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:02:29.313Z", + baseModelName: null, }, "gemini-3.1-flash-lite-preview": { - "provider": "google", - "description": "Google's most cost-efficient multimodal model in the Gemini 3 series, optimized for high-volume, low-latency tasks like translation, classification, and simple data extraction. Offers 2.5x faster time-to-first-token than Gemini 2.5 Flash.", - "contextWindow": 1048576, - "maxOutputTokens": 65536, - "capabilities": [ + provider: "google", + description: + "Google's most cost-efficient multimodal model in the Gemini 3 series, optimized for high-volume, low-latency tasks like translation, classification, and simple data extraction. Offers 2.5x faster time-to-first-token than Gemini 2.5 Flash.", + contextWindow: 1048576, + maxOutputTokens: 65536, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2026-03-03", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:02:29.253Z", - "baseModelName": null + releaseDate: "2026-03-03", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:02:29.253Z", + baseModelName: null, }, "gemini-3.1-pro-preview": { - "provider": "google", - "description": "Google's most advanced reasoning model in the Gemini 3.1 family, excelling at complex problem-solving across text, audio, images, video, and code with a 1M token context window and extended thinking capabilities.", - "contextWindow": 1048576, - "maxOutputTokens": 65536, - "capabilities": [ + provider: "google", + description: + "Google's most advanced reasoning model in the Gemini 3.1 family, excelling at complex problem-solving across text, audio, images, video, and code with a 1M token context window and extended thinking capabilities.", + contextWindow: 1048576, + maxOutputTokens: 65536, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2026-02-19", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:03:33.071Z", - "baseModelName": null + releaseDate: "2026-02-19", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:03:33.071Z", + baseModelName: null, }, "gemini-pro": { - "provider": "google", - "description": "Google's first-generation Gemini model for text generation, reasoning, and multi-turn conversation. Superseded by Gemini 1.5 Pro and later models.", - "contextWindow": 32768, - "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-12-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-04-09", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T11:03:45.401Z", - "baseModelName": null + provider: "google", + description: + "Google's first-generation Gemini model for text generation, reasoning, and multi-turn conversation. Superseded by Gemini 1.5 Pro and later models.", + contextWindow: 32768, + maxOutputTokens: 8192, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-12-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: false, + deprecationDate: "2025-04-09", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T11:03:45.401Z", + baseModelName: null, }, "gpt-3.5-turbo": { - "provider": "openai", - "description": "OpenAI's fast and cost-effective model optimized for chat and instruction-following tasks, now superseded by GPT-4o mini.", - "contextWindow": 16385, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2023-03-01", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:03:11.412Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's fast and cost-effective model optimized for chat and instruction-following tasks, now superseded by GPT-4o mini.", + contextWindow: 16385, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2023-03-01", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:03:11.412Z", + baseModelName: null, }, "gpt-3.5-turbo-0125": { - "provider": "openai", - "description": "A fast and cost-effective GPT-3.5 Turbo snapshot optimized for chat completions, offering improved accuracy for function calling and reduced instances of incomplete responses.", - "contextWindow": 16385, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2024-01-25", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:03:11.310Z", - "baseModelName": null + provider: "openai", + description: + "A fast and cost-effective GPT-3.5 Turbo snapshot optimized for chat completions, offering improved accuracy for function calling and reduced instances of incomplete responses.", + contextWindow: 16385, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2024-01-25", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:03:11.310Z", + baseModelName: null, }, "gpt-3.5-turbo-0301": { - "provider": "openai", - "description": "Early snapshot of GPT-3.5 Turbo, OpenAI's first ChatGPT-optimized model for chat completions. Fast and cost-effective for simple tasks but superseded by later revisions.", - "contextWindow": 4096, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "fine_tunable" - ], - "releaseDate": "2023-03-01", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2024-06-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:03:12.060Z", - "baseModelName": null + provider: "openai", + description: + "Early snapshot of GPT-3.5 Turbo, OpenAI's first ChatGPT-optimized model for chat completions. Fast and cost-effective for simple tasks but superseded by later revisions.", + contextWindow: 4096, + maxOutputTokens: 4096, + capabilities: ["streaming", "fine_tunable"], + releaseDate: "2023-03-01", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2024-06-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:03:12.060Z", + baseModelName: null, }, "gpt-3.5-turbo-0613": { - "provider": "openai", - "description": "A snapshot of GPT-3.5 Turbo from June 2023, optimized for chat and instruction-following tasks with function calling support.", - "contextWindow": 4096, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2023-06-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2024-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:04:04.463Z", - "baseModelName": null + provider: "openai", + description: + "A snapshot of GPT-3.5 Turbo from June 2023, optimized for chat and instruction-following tasks with function calling support.", + contextWindow: 4096, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2023-06-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2024-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:04:04.463Z", + baseModelName: null, }, "gpt-3.5-turbo-1106": { - "provider": "openai", - "description": "A dated snapshot of GPT-3.5 Turbo released in November 2023, offering improved instruction following, JSON mode, and parallel function calling over previous GPT-3.5 variants.", - "contextWindow": 16385, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2023-11-06", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:04:23.054Z", - "baseModelName": null + provider: "openai", + description: + "A dated snapshot of GPT-3.5 Turbo released in November 2023, offering improved instruction following, JSON mode, and parallel function calling over previous GPT-3.5 variants.", + contextWindow: 16385, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2023-11-06", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:04:23.054Z", + baseModelName: null, }, "gpt-3.5-turbo-16k": { - "provider": "openai", - "description": "Extended context version of GPT-3.5 Turbo with 16K token context window, offering the same capabilities as the base model but able to process longer inputs.", - "contextWindow": 16384, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "json_mode", - "fine_tunable", - "tool_use" - ], - "releaseDate": "2023-06-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:04:36.307Z", - "baseModelName": null + provider: "openai", + description: + "Extended context version of GPT-3.5 Turbo with 16K token context window, offering the same capabilities as the base model but able to process longer inputs.", + contextWindow: 16384, + maxOutputTokens: 4096, + capabilities: ["streaming", "json_mode", "fine_tunable", "tool_use"], + releaseDate: "2023-06-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:04:36.307Z", + baseModelName: null, }, "gpt-3.5-turbo-16k-0613": { - "provider": "openai", - "description": "Extended context window variant of GPT-3.5 Turbo with 16K token context, snapshot from June 2023. Optimized for chat completions with longer document processing.", - "contextWindow": 16384, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2023-06-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2024-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:04:22.894Z", - "baseModelName": null + provider: "openai", + description: + "Extended context window variant of GPT-3.5 Turbo with 16K token context, snapshot from June 2023. Optimized for chat completions with longer document processing.", + contextWindow: 16384, + maxOutputTokens: 4096, + capabilities: ["streaming", "json_mode", "fine_tunable"], + releaseDate: "2023-06-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2024-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:04:22.894Z", + baseModelName: null, }, "gpt-3.5-turbo-instruct": { - "provider": "openai", - "description": "OpenAI's GPT-3.5 Turbo Instruct is a completions-only model (not chat) optimized for following explicit instructions, replacing the legacy text-davinci-003 model.", - "contextWindow": 4096, - "maxOutputTokens": 4096, - "capabilities": [ - "fine_tunable" - ], - "releaseDate": "2023-09-19", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-01-27", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:04:22.309Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's GPT-3.5 Turbo Instruct is a completions-only model (not chat) optimized for following explicit instructions, replacing the legacy text-davinci-003 model.", + contextWindow: 4096, + maxOutputTokens: 4096, + capabilities: ["fine_tunable"], + releaseDate: "2023-09-19", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-01-27", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:04:22.309Z", + baseModelName: null, }, "gpt-4": { - "provider": "openai", - "description": "OpenAI's flagship large language model that preceded GPT-4o, known for strong reasoning and instruction-following capabilities across a wide range of tasks.", - "contextWindow": 8192, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-06-06", - "knowledgeCutoff": "2023-12-01", - "resolvedAt": "2026-03-24T11:04:36.773Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's flagship large language model that preceded GPT-4o, known for strong reasoning and instruction-following capabilities across a wide range of tasks.", + contextWindow: 8192, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-06-06", + knowledgeCutoff: "2023-12-01", + resolvedAt: "2026-03-24T11:04:36.773Z", + baseModelName: null, }, "gpt-4-0125-preview": { - "provider": "openai", - "description": "An improved GPT-4 Turbo preview model with better task completion, reduced laziness in code generation, and enhanced instruction following.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-01-25", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-12-01", - "resolvedAt": "2026-03-24T11:04:54.196Z", - "baseModelName": null + provider: "openai", + description: + "An improved GPT-4 Turbo preview model with better task completion, reduced laziness in code generation, and enhanced instruction following.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2024-01-25", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-12-01", + resolvedAt: "2026-03-24T11:04:54.196Z", + baseModelName: null, }, "gpt-4-0314": { - "provider": "openai", - "description": "Original GPT-4 snapshot from March 2023, a large multimodal model (text-only at launch) that was one of OpenAI's first GPT-4 releases. Now deprecated and replaced by newer GPT-4 variants.", - "contextWindow": 8192, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2024-06-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:05:14.112Z", - "baseModelName": null + provider: "openai", + description: + "Original GPT-4 snapshot from March 2023, a large multimodal model (text-only at launch) that was one of OpenAI's first GPT-4 releases. Now deprecated and replaced by newer GPT-4 variants.", + contextWindow: 8192, + maxOutputTokens: 4096, + capabilities: ["streaming"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2024-06-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:05:14.112Z", + baseModelName: null, }, "gpt-4-0613": { - "provider": "openai", - "description": "A snapshot of GPT-4 from June 2023, offering strong reasoning and instruction-following capabilities. It was one of the first widely available GPT-4 variants with function calling support.", - "contextWindow": 8192, - "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2023-06-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-06-06", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:05:13.885Z", - "baseModelName": null + provider: "openai", + description: + "A snapshot of GPT-4 from June 2023, offering strong reasoning and instruction-following capabilities. It was one of the first widely available GPT-4 variants with function calling support.", + contextWindow: 8192, + maxOutputTokens: 8192, + capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2023-06-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-06-06", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:05:13.885Z", + baseModelName: null, }, "gpt-4-1106-preview": { - "provider": "openai", - "description": "GPT-4 Turbo preview model with 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-11-06", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T11:05:12.960Z", - "baseModelName": null + provider: "openai", + description: + "GPT-4 Turbo preview model with 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2023-11-06", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T11:05:12.960Z", + baseModelName: null, }, "gpt-4-32k": { - "provider": "openai", - "description": "Extended context window variant of GPT-4 with 32,768 token capacity, offering the same capabilities as GPT-4 but able to process longer documents and conversations.", - "contextWindow": 32768, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-06-06", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:05:14.584Z", - "baseModelName": null + provider: "openai", + description: + "Extended context window variant of GPT-4 with 32,768 token capacity, offering the same capabilities as GPT-4 but able to process longer documents and conversations.", + contextWindow: 32768, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-06-06", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:05:14.584Z", + baseModelName: null, }, "gpt-4-32k-0314": { - "provider": "openai", - "description": "Extended context (32k token) variant of the original GPT-4 launch snapshot from March 2024, offering the same capabilities as gpt-4-0314 but with 4x the context window.", - "contextWindow": 32768, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2024-06-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:05:32.044Z", - "baseModelName": null + provider: "openai", + description: + "Extended context (32k token) variant of the original GPT-4 launch snapshot from March 2024, offering the same capabilities as gpt-4-0314 but with 4x the context window.", + contextWindow: 32768, + maxOutputTokens: 4096, + capabilities: ["streaming"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2024-06-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:05:32.044Z", + baseModelName: null, }, "gpt-4-32k-0613": { - "provider": "openai", - "description": "Extended context window variant of GPT-4 with 32,768 token context, based on the June 2023 snapshot. Offers the same capabilities as GPT-4 but with 4x the context length.", - "contextWindow": 32768, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-06-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-06-06", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:05:53.070Z", - "baseModelName": null + provider: "openai", + description: + "Extended context window variant of GPT-4 with 32,768 token context, based on the June 2023 snapshot. Offers the same capabilities as GPT-4 but with 4x the context length.", + contextWindow: 32768, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-06-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-06-06", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:05:53.070Z", + baseModelName: null, }, "gpt-4-preview": { - "provider": "openai", - "description": "GPT-4 Turbo preview model with 128K context window, JSON mode, and parallel function calling. A preview release in the GPT-4 Turbo series, now deprecated in favor of newer models.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-11-06", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-06-06", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T11:06:54.248Z", - "baseModelName": null + provider: "openai", + description: + "GPT-4 Turbo preview model with 128K context window, JSON mode, and parallel function calling. A preview release in the GPT-4 Turbo series, now deprecated in favor of newer models.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-11-06", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-06-06", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T11:06:54.248Z", + baseModelName: null, }, "gpt-4-turbo": { - "provider": "openai", - "description": "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-04-09", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-12-01", - "resolvedAt": "2026-03-24T11:05:51.415Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-04-09", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-12-01", + resolvedAt: "2026-03-24T11:05:51.415Z", + baseModelName: null, }, "gpt-4-turbo-2024-04-09": { - "provider": "openai", - "description": "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-04-09", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-12-01", - "resolvedAt": "2026-03-24T11:05:51.415Z", - "baseModelName": "gpt-4-turbo" + provider: "openai", + description: + "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-04-09", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-12-01", + resolvedAt: "2026-03-24T11:05:51.415Z", + baseModelName: "gpt-4-turbo", }, "gpt-4-turbo-preview": { - "provider": "openai", - "description": "An early preview of GPT-4 Turbo with a 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-01-25", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-12-01", - "resolvedAt": "2026-03-24T11:05:52.346Z", - "baseModelName": null + provider: "openai", + description: + "An early preview of GPT-4 Turbo with a 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-01-25", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-12-01", + resolvedAt: "2026-03-24T11:05:52.346Z", + baseModelName: null, }, "gpt-4-turbo-vision": { - "provider": "openai", - "description": "OpenAI's GPT-4 Turbo model with vision capabilities, able to analyze and understand images alongside text. It was a preview model later superseded by GPT-4 Turbo (gpt-4-turbo-2024-04-09) and then GPT-4o.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-11-06", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2024-12-06", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T11:06:38.455Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's GPT-4 Turbo model with vision capabilities, able to analyze and understand images alongside text. It was a preview model later superseded by GPT-4 Turbo (gpt-4-turbo-2024-04-09) and then GPT-4o.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2023-11-06", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2024-12-06", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T11:06:38.455Z", + baseModelName: null, }, "gpt-4.1": { - "provider": "openai", - "description": "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", - "contextWindow": 1047576, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:07:00.439Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", + contextWindow: 1047576, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:07:00.439Z", + baseModelName: null, }, "gpt-4.1-2025-04-14": { - "provider": "openai", - "description": "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", - "contextWindow": 1047576, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:07:00.439Z", - "baseModelName": "gpt-4.1" + provider: "openai", + description: + "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", + contextWindow: 1047576, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:07:00.439Z", + baseModelName: "gpt-4.1", }, "gpt-4.1-mini": { - "provider": "openai", - "description": "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", - "contextWindow": 1000000, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:08:14.524Z", - "baseModelName": null + provider: "openai", + description: + "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", + contextWindow: 1000000, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:08:14.524Z", + baseModelName: null, }, "gpt-4.1-mini-2025-04-14": { - "provider": "openai", - "description": "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", - "contextWindow": 1000000, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:08:14.524Z", - "baseModelName": "gpt-4.1-mini" + provider: "openai", + description: + "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", + contextWindow: 1000000, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:08:14.524Z", + baseModelName: "gpt-4.1-mini", }, "gpt-4.1-nano": { - "provider": "openai", - "description": "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", - "contextWindow": 1047576, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:08:04.533Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", + contextWindow: 1047576, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:08:04.533Z", + baseModelName: null, }, "gpt-4.1-nano-2025-04-14": { - "provider": "openai", - "description": "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", - "contextWindow": 1047576, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:08:04.533Z", - "baseModelName": "gpt-4.1-nano" + provider: "openai", + description: + "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", + contextWindow: 1047576, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:08:04.533Z", + baseModelName: "gpt-4.1-nano", }, "gpt-4.5-preview": { - "provider": "openai", - "description": "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-02-27", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-07-14", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:57.880Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-02-27", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-07-14", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:57.880Z", + baseModelName: null, }, "gpt-4.5-preview-2025-02-27": { - "provider": "openai", - "description": "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-02-27", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-07-14", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:57.880Z", - "baseModelName": "gpt-4.5-preview" + provider: "openai", + description: + "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-02-27", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-07-14", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:57.880Z", + baseModelName: "gpt-4.5-preview", }, "gpt-4o": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable" + "fine_tunable", ], - "releaseDate": "2024-05-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:31.638Z", - "baseModelName": null + releaseDate: "2024-05-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:31.638Z", + baseModelName: null, }, "gpt-4o-2024-05-13": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable" + "fine_tunable", ], - "releaseDate": "2024-05-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:31.638Z", - "baseModelName": "gpt-4o" + releaseDate: "2024-05-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:31.638Z", + baseModelName: "gpt-4o", }, "gpt-4o-2024-08-06": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable" + "fine_tunable", ], - "releaseDate": "2024-05-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:31.638Z", - "baseModelName": "gpt-4o" + releaseDate: "2024-05-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:31.638Z", + baseModelName: "gpt-4o", }, "gpt-4o-2024-11-20": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable" + "fine_tunable", ], - "releaseDate": "2024-05-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:31.638Z", - "baseModelName": "gpt-4o" + releaseDate: "2024-05-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:31.638Z", + baseModelName: "gpt-4o", }, "gpt-4o-audio-preview": { - "provider": "openai", - "description": "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], - "releaseDate": "2024-10-01", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-05-07", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:08:09.590Z", - "baseModelName": null + provider: "openai", + description: + "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], + releaseDate: "2024-10-01", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-05-07", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:08:09.590Z", + baseModelName: null, }, "gpt-4o-audio-preview-2024-10-01": { - "provider": "openai", - "description": "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], - "releaseDate": "2024-10-01", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-05-07", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:08:09.590Z", - "baseModelName": "gpt-4o-audio-preview" + provider: "openai", + description: + "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], + releaseDate: "2024-10-01", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-05-07", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:08:09.590Z", + baseModelName: "gpt-4o-audio-preview", }, "gpt-4o-mini": { - "provider": "openai", - "description": "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2024-07-18", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:09:50.130Z", - "baseModelName": null + provider: "openai", + description: + "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2024-07-18", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:09:50.130Z", + baseModelName: null, }, "gpt-4o-mini-2024-07-18": { - "provider": "openai", - "description": "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2024-07-18", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:09:50.130Z", - "baseModelName": "gpt-4o-mini" + provider: "openai", + description: + "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2024-07-18", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:09:50.130Z", + baseModelName: "gpt-4o-mini", }, "gpt-4o-realtime-preview": { - "provider": "openai", - "description": "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], - "releaseDate": "2024-10-01", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": false, - "deprecationDate": "2026-05-07", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:09:35.495Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], + releaseDate: "2024-10-01", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: false, + deprecationDate: "2026-05-07", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:09:35.495Z", + baseModelName: null, }, "gpt-4o-realtime-preview-2024-10-01": { - "provider": "openai", - "description": "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], - "releaseDate": "2024-10-01", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": false, - "deprecationDate": "2026-05-07", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:09:35.495Z", - "baseModelName": "gpt-4o-realtime-preview" + provider: "openai", + description: + "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], + releaseDate: "2024-10-01", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: false, + deprecationDate: "2026-05-07", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:09:35.495Z", + baseModelName: "gpt-4o-realtime-preview", }, "gpt-5": { - "provider": "openai", - "description": "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "image_generation", - "code_execution" + "code_execution", ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-09-30", - "resolvedAt": "2026-03-24T11:09:28.216Z", - "baseModelName": null + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-09-30", + resolvedAt: "2026-03-24T11:09:28.216Z", + baseModelName: null, }, "gpt-5-2025-08-07": { - "provider": "openai", - "description": "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "image_generation", - "code_execution" + "code_execution", ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-09-30", - "resolvedAt": "2026-03-24T11:09:28.216Z", - "baseModelName": "gpt-5" + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-09-30", + resolvedAt: "2026-03-24T11:09:28.216Z", + baseModelName: "gpt-5", }, "gpt-5-chat-latest": { - "provider": "openai", - "description": "Non-reasoning GPT-5 model used in ChatGPT, optimized for conversational tasks. Supports text and image inputs with function calling and structured outputs.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-09-30", - "resolvedAt": "2026-03-24T11:09:24.834Z", - "baseModelName": "gpt-5-chat" + provider: "openai", + description: + "Non-reasoning GPT-5 model used in ChatGPT, optimized for conversational tasks. Supports text and image inputs with function calling and structured outputs.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-09-30", + resolvedAt: "2026-03-24T11:09:24.834Z", + baseModelName: "gpt-5-chat", }, "gpt-5-mini": { - "provider": "openai", - "description": "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-05-31", - "resolvedAt": "2026-03-24T11:09:42.822Z", - "baseModelName": null + provider: "openai", + description: + "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-05-31", + resolvedAt: "2026-03-24T11:09:42.822Z", + baseModelName: null, }, "gpt-5-mini-2025-08-07": { - "provider": "openai", - "description": "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-05-31", - "resolvedAt": "2026-03-24T11:09:42.822Z", - "baseModelName": "gpt-5-mini" + provider: "openai", + description: + "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-05-31", + resolvedAt: "2026-03-24T11:09:42.822Z", + baseModelName: "gpt-5-mini", }, "gpt-5-nano": { - "provider": "openai", - "description": "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-05-31", - "resolvedAt": "2026-03-24T11:11:24.884Z", - "baseModelName": null + provider: "openai", + description: + "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-05-31", + resolvedAt: "2026-03-24T11:11:24.884Z", + baseModelName: null, }, "gpt-5-nano-2025-08-07": { - "provider": "openai", - "description": "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-05-31", - "resolvedAt": "2026-03-24T11:11:24.884Z", - "baseModelName": "gpt-5-nano" + provider: "openai", + description: + "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-05-31", + resolvedAt: "2026-03-24T11:11:24.884Z", + baseModelName: "gpt-5-nano", }, "gpt-5-pro": { - "provider": "openai", - "description": "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-10-06", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-10-01", - "resolvedAt": "2026-03-24T11:11:37.048Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-10-06", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-10-01", + resolvedAt: "2026-03-24T11:11:37.048Z", + baseModelName: null, }, "gpt-5-pro-2025-10-06": { - "provider": "openai", - "description": "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-10-06", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-10-01", - "resolvedAt": "2026-03-24T11:11:37.048Z", - "baseModelName": "gpt-5-pro" + provider: "openai", + description: + "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-10-06", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-10-01", + resolvedAt: "2026-03-24T11:11:37.048Z", + baseModelName: "gpt-5-pro", }, "gpt-5.1": { - "provider": "openai", - "description": "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-11-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-09-30", - "resolvedAt": "2026-03-24T11:11:47.327Z", - "baseModelName": null + provider: "openai", + description: + "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-11-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-09-30", + resolvedAt: "2026-03-24T11:11:47.327Z", + baseModelName: null, }, "gpt-5.1-2025-11-13": { - "provider": "openai", - "description": "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-11-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-09-30", - "resolvedAt": "2026-03-24T11:11:47.327Z", - "baseModelName": "gpt-5.1" + provider: "openai", + description: + "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-11-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-09-30", + resolvedAt: "2026-03-24T11:11:47.327Z", + baseModelName: "gpt-5.1", }, "gpt-5.2": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-12-11", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:11:13.129Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-12-11", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:11:13.129Z", + baseModelName: null, }, "gpt-5.2-2025-12-11": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-12-11", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:11:13.129Z", - "baseModelName": "gpt-5.2" + provider: "openai", + description: + "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-12-11", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:11:13.129Z", + baseModelName: "gpt-5.2", }, "gpt-5.2-pro": { - "provider": "openai", - "description": "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], - "releaseDate": "2025-12-11", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:11:12.711Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], + releaseDate: "2025-12-11", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:11:12.711Z", + baseModelName: null, }, "gpt-5.2-pro-2025-12-11": { - "provider": "openai", - "description": "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], - "releaseDate": "2025-12-11", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:11:12.711Z", - "baseModelName": "gpt-5.2-pro" + provider: "openai", + description: + "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], + releaseDate: "2025-12-11", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:11:12.711Z", + baseModelName: "gpt-5.2-pro", }, "gpt-5.4": { - "provider": "openai", - "description": "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", - "contextWindow": 1050000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "code_execution" - ], - "releaseDate": "2026-03-05", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:09.220Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", + contextWindow: 1050000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "code_execution"], + releaseDate: "2026-03-05", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:09.220Z", + baseModelName: null, }, "gpt-5.4-2026-03-05": { - "provider": "openai", - "description": "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", - "contextWindow": 1050000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "code_execution" - ], - "releaseDate": "2026-03-05", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:09.220Z", - "baseModelName": "gpt-5.4" + provider: "openai", + description: + "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", + contextWindow: 1050000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "code_execution"], + releaseDate: "2026-03-05", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:09.220Z", + baseModelName: "gpt-5.4", }, "gpt-5.4-mini": { - "provider": "openai", - "description": "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2026-03-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:35.473Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2026-03-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:35.473Z", + baseModelName: null, }, "gpt-5.4-mini-2026-03-17": { - "provider": "openai", - "description": "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2026-03-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:35.473Z", - "baseModelName": "gpt-5.4-mini" + provider: "openai", + description: + "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2026-03-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:35.473Z", + baseModelName: "gpt-5.4-mini", }, "gpt-5.4-nano": { - "provider": "openai", - "description": "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2026-03-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:52.285Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2026-03-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:52.285Z", + baseModelName: null, }, "gpt-5.4-nano-2026-03-17": { - "provider": "openai", - "description": "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2026-03-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:52.285Z", - "baseModelName": "gpt-5.4-nano" + provider: "openai", + description: + "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2026-03-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:52.285Z", + baseModelName: "gpt-5.4-nano", }, "gpt-5.4-pro": { - "provider": "openai", - "description": "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", - "contextWindow": 1050000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], - "releaseDate": "2026-03-05", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:56.903Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", + contextWindow: 1050000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], + releaseDate: "2026-03-05", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:56.903Z", + baseModelName: null, }, "gpt-5.4-pro-2026-03-05": { - "provider": "openai", - "description": "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", - "contextWindow": 1050000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], - "releaseDate": "2026-03-05", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:56.903Z", - "baseModelName": "gpt-5.4-pro" - }, - "o1": { - "provider": "openai", - "description": "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-12-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:23.948Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", + contextWindow: 1050000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], + releaseDate: "2026-03-05", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:56.903Z", + baseModelName: "gpt-5.4-pro", + }, + o1: { + provider: "openai", + description: + "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-12-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:23.948Z", + baseModelName: null, }, "o1-2024-12-17": { - "provider": "openai", - "description": "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-12-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:23.948Z", - "baseModelName": "o1" + provider: "openai", + description: + "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-12-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:23.948Z", + baseModelName: "o1", }, "o1-mini": { - "provider": "openai", - "description": "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", - "contextWindow": 128000, - "maxOutputTokens": 65536, - "capabilities": [ - "streaming", - "json_mode" - ], - "releaseDate": "2024-09-12", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-06-30", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:37.030Z", - "baseModelName": null + provider: "openai", + description: + "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", + contextWindow: 128000, + maxOutputTokens: 65536, + capabilities: ["streaming", "json_mode"], + releaseDate: "2024-09-12", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-06-30", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:37.030Z", + baseModelName: null, }, "o1-mini-2024-09-12": { - "provider": "openai", - "description": "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", - "contextWindow": 128000, - "maxOutputTokens": 65536, - "capabilities": [ - "streaming", - "json_mode" - ], - "releaseDate": "2024-09-12", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-06-30", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:37.030Z", - "baseModelName": "o1-mini" + provider: "openai", + description: + "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", + contextWindow: 128000, + maxOutputTokens: 65536, + capabilities: ["streaming", "json_mode"], + releaseDate: "2024-09-12", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-06-30", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:37.030Z", + baseModelName: "o1-mini", }, "o1-preview": { - "provider": "openai", - "description": "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", - "contextWindow": 128000, - "maxOutputTokens": 32768, - "capabilities": [ - "streaming" - ], - "releaseDate": "2024-09-12", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-10-31", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:59.198Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", + contextWindow: 128000, + maxOutputTokens: 32768, + capabilities: ["streaming"], + releaseDate: "2024-09-12", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-10-31", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:59.198Z", + baseModelName: null, }, "o1-preview-2024-09-12": { - "provider": "openai", - "description": "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", - "contextWindow": 128000, - "maxOutputTokens": 32768, - "capabilities": [ - "streaming" - ], - "releaseDate": "2024-09-12", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-10-31", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:59.198Z", - "baseModelName": "o1-preview" + provider: "openai", + description: + "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", + contextWindow: 128000, + maxOutputTokens: 32768, + capabilities: ["streaming"], + releaseDate: "2024-09-12", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-10-31", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:59.198Z", + baseModelName: "o1-preview", }, "o1-pro": { - "provider": "openai", - "description": "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-03-19", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:13:57.532Z", - "baseModelName": null + provider: "openai", + description: + "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "json_mode", "extended_thinking"], + releaseDate: "2025-03-19", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:13:57.532Z", + baseModelName: null, }, "o1-pro-2025-03-19": { - "provider": "openai", - "description": "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-03-19", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:13:57.532Z", - "baseModelName": "o1-pro" - }, - "o3": { - "provider": "openai", - "description": "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-04-16", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:04.906Z", - "baseModelName": null + provider: "openai", + description: + "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "json_mode", "extended_thinking"], + releaseDate: "2025-03-19", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:13:57.532Z", + baseModelName: "o1-pro", + }, + o3: { + provider: "openai", + description: + "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-04-16", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:04.906Z", + baseModelName: null, }, "o3-2025-04-16": { - "provider": "openai", - "description": "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-04-16", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:04.906Z", - "baseModelName": "o3" + provider: "openai", + description: + "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-04-16", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:04.906Z", + baseModelName: "o3", }, "o3-mini": { - "provider": "openai", - "description": "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-01-31", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:13:33.788Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-01-31", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:13:33.788Z", + baseModelName: null, }, "o3-mini-2025-01-31": { - "provider": "openai", - "description": "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-01-31", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:13:33.788Z", - "baseModelName": "o3-mini" + provider: "openai", + description: + "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-01-31", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:13:33.788Z", + baseModelName: "o3-mini", }, "o3-pro": { - "provider": "openai", - "description": "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-06-10", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:10.900Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-06-10", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:10.900Z", + baseModelName: null, }, "o3-pro-2025-06-10": { - "provider": "openai", - "description": "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-06-10", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:10.900Z", - "baseModelName": "o3-pro" + provider: "openai", + description: + "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-06-10", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:10.900Z", + baseModelName: "o3-pro", }, "o4-mini": { - "provider": "openai", - "description": "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-04-16", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:16.050Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-04-16", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:16.050Z", + baseModelName: null, }, "o4-mini-2025-04-16": { - "provider": "openai", - "description": "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-04-16", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:16.050Z", - "baseModelName": "o4-mini" - } + provider: "openai", + description: + "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-04-16", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:16.050Z", + baseModelName: "o4-mini", + }, }; diff --git a/internal-packages/llm-model-catalog/src/registry.test.ts b/internal-packages/llm-model-catalog/src/registry.test.ts index 349ba2622e..cfda717727 100644 --- a/internal-packages/llm-model-catalog/src/registry.test.ts +++ b/internal-packages/llm-model-catalog/src/registry.test.ts @@ -303,9 +303,7 @@ describe("ModelPricingRegistry", () => { name: "Large Context", isDefault: false, priority: 0, - conditions: [ - { usageDetailPattern: "input", operator: "gt" as const, value: 200000 }, - ], + conditions: [{ usageDetailPattern: "input", operator: "gt" as const, value: 200000 }], prices: [ { usageType: "input", price: 0.0000025 }, { usageType: "output", price: 0.00001 }, @@ -371,9 +369,7 @@ describe("ModelPricingRegistry", () => { name: "Conditional", isDefault: false, priority: 1, - conditions: [ - { usageDetailPattern: "input", operator: "gt" as const, value: 100 }, - ], + conditions: [{ usageDetailPattern: "input", operator: "gt" as const, value: 100 }], prices: [{ usageType: "input", price: 0.0001 }], }, { @@ -405,9 +401,7 @@ describe("ModelPricingRegistry", () => { name: "Conditional", isDefault: false, priority: 0, - conditions: [ - { usageDetailPattern: "input", operator: "gt" as const, value: 999999 }, - ], + conditions: [{ usageDetailPattern: "input", operator: "gt" as const, value: 999999 }], prices: [{ usageType: "input", price: 0.001 }], }, { diff --git a/internal-packages/llm-model-catalog/src/registry.ts b/internal-packages/llm-model-catalog/src/registry.ts index 6a3d52814c..880bed3951 100644 --- a/internal-packages/llm-model-catalog/src/registry.ts +++ b/internal-packages/llm-model-catalog/src/registry.ts @@ -134,10 +134,7 @@ export class ModelPricingRegistry { return null; } - calculateCost( - responseModel: string, - usageDetails: Record - ): LlmCostResult | null { + calculateCost(responseModel: string, usageDetails: Record): LlmCostResult | null { const model = this.match(responseModel); if (!model) return null; diff --git a/internal-packages/llm-model-catalog/src/sync.test.ts b/internal-packages/llm-model-catalog/src/sync.test.ts index d2138564ef..a5c6ed4841 100644 --- a/internal-packages/llm-model-catalog/src/sync.test.ts +++ b/internal-packages/llm-model-catalog/src/sync.test.ts @@ -32,10 +32,7 @@ const STALE_BASE_MODEL_NAME = "wrong-base-model-sentinel"; const STALE_INPUT_PRICE = 0.099; const STALE_OUTPUT_PRICE = 0.088; -async function createGpt4oWithStalePricing( - prisma: PrismaClient, - source: "default" | "admin" -) { +async function createGpt4oWithStalePricing(prisma: PrismaClient, source: "default" | "admin") { const model = await prisma.llmModel.create({ data: { friendlyId: generateFriendlyId("llm_model"), @@ -129,7 +126,9 @@ async function loadGpt4oWithTiers(prisma: PrismaClient) { }); } -function expectBundledGpt4oPricing(model: NonNullable>>) { +function expectBundledGpt4oPricing( + model: NonNullable>> +) { expect(model.matchPattern).toBe(gpt4oDef.matchPattern); expect(model.pricingTiers).toHaveLength(gpt4oDef.pricingTiers.length); diff --git a/internal-packages/llm-model-catalog/src/sync.ts b/internal-packages/llm-model-catalog/src/sync.ts index 8761df0fe4..378a9bf9f2 100644 --- a/internal-packages/llm-model-catalog/src/sync.ts +++ b/internal-packages/llm-model-catalog/src/sync.ts @@ -71,9 +71,7 @@ export async function syncLlmCatalog(prisma: PrismaClient): Promise<{ capabilities: catalog?.capabilities ?? existing.capabilities, isHidden: catalog?.isHidden ?? existing.isHidden, baseModelName: - catalog?.baseModelName === undefined - ? existing.baseModelName - : catalog.baseModelName, + catalog?.baseModelName === undefined ? existing.baseModelName : catalog.baseModelName, pricingUnit: existing.pricingUnit ?? "tokens", }, }); diff --git a/internal-packages/otlp-importer/package.json b/internal-packages/otlp-importer/package.json index 6f5cd39665..5c488ce1aa 100644 --- a/internal-packages/otlp-importer/package.json +++ b/internal-packages/otlp-importer/package.json @@ -38,4 +38,4 @@ "long": "^5.2.3", "protobufjs": "^7.2.6" } -} \ No newline at end of file +} 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 fa4a1e3666..50d82933e0 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 @@ -108,10 +108,14 @@ export const ExportLogsServiceRequest = { return obj; }, - create, I>>(base?: I): ExportLogsServiceRequest { + create, I>>( + base?: I + ): ExportLogsServiceRequest { return ExportLogsServiceRequest.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportLogsServiceRequest { + fromPartial, I>>( + object: I + ): ExportLogsServiceRequest { const message = createBaseExportLogsServiceRequest(); message.resourceLogs = object.resourceLogs?.map((e) => ResourceLogs.fromPartial(e)) || []; return message; @@ -169,14 +173,19 @@ export const ExportLogsServiceResponse = { return obj; }, - create, I>>(base?: I): ExportLogsServiceResponse { + create, I>>( + base?: I + ): ExportLogsServiceResponse { return ExportLogsServiceResponse.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportLogsServiceResponse { + fromPartial, I>>( + object: I + ): ExportLogsServiceResponse { const message = createBaseExportLogsServiceResponse(); - message.partialSuccess = (object.partialSuccess !== undefined && object.partialSuccess !== null) - ? ExportLogsPartialSuccess.fromPartial(object.partialSuccess) - : undefined; + message.partialSuccess = + object.partialSuccess !== undefined && object.partialSuccess !== null + ? ExportLogsPartialSuccess.fromPartial(object.partialSuccess) + : undefined; return message; }, }; @@ -189,7 +198,9 @@ export const ExportLogsPartialSuccess = { encode(message: ExportLogsPartialSuccess, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.rejectedLogRecords !== BigInt("0")) { if (BigInt.asIntN(64, message.rejectedLogRecords) !== message.rejectedLogRecords) { - throw new globalThis.Error("value provided for field message.rejectedLogRecords of type int64 too large"); + throw new globalThis.Error( + "value provided for field message.rejectedLogRecords of type int64 too large" + ); } writer.uint32(8).int64(message.rejectedLogRecords.toString()); } @@ -231,7 +242,9 @@ export const ExportLogsPartialSuccess = { fromJSON(object: any): ExportLogsPartialSuccess { return { - rejectedLogRecords: isSet(object.rejectedLogRecords) ? BigInt(object.rejectedLogRecords) : BigInt("0"), + rejectedLogRecords: isSet(object.rejectedLogRecords) + ? BigInt(object.rejectedLogRecords) + : BigInt("0"), errorMessage: isSet(object.errorMessage) ? globalThis.String(object.errorMessage) : "", }; }, @@ -247,10 +260,14 @@ export const ExportLogsPartialSuccess = { return obj; }, - create, I>>(base?: I): ExportLogsPartialSuccess { + create, I>>( + base?: I + ): ExportLogsPartialSuccess { return ExportLogsPartialSuccess.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportLogsPartialSuccess { + fromPartial, I>>( + object: I + ): ExportLogsPartialSuccess { const message = createBaseExportLogsPartialSuccess(); message.rejectedLogRecords = object.rejectedLogRecords ?? BigInt("0"); message.errorMessage = object.errorMessage ?? ""; @@ -293,14 +310,19 @@ interface Rpc { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { 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 53bda7b01d..9f7c913c34 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 @@ -62,7 +62,10 @@ function createBaseExportMetricsServiceRequest(): ExportMetricsServiceRequest { } export const ExportMetricsServiceRequest = { - encode(message: ExportMetricsServiceRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExportMetricsServiceRequest, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { for (const v of message.resourceMetrics) { ResourceMetrics.encode(v!, writer.uint32(10).fork()).ldelim(); } @@ -108,12 +111,17 @@ export const ExportMetricsServiceRequest = { return obj; }, - create, I>>(base?: I): ExportMetricsServiceRequest { + create, I>>( + base?: I + ): ExportMetricsServiceRequest { return ExportMetricsServiceRequest.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportMetricsServiceRequest { + fromPartial, I>>( + object: I + ): ExportMetricsServiceRequest { const message = createBaseExportMetricsServiceRequest(); - message.resourceMetrics = object.resourceMetrics?.map((e) => ResourceMetrics.fromPartial(e)) || []; + message.resourceMetrics = + object.resourceMetrics?.map((e) => ResourceMetrics.fromPartial(e)) || []; return message; }, }; @@ -123,7 +131,10 @@ function createBaseExportMetricsServiceResponse(): ExportMetricsServiceResponse } export const ExportMetricsServiceResponse = { - encode(message: ExportMetricsServiceResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExportMetricsServiceResponse, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { if (message.partialSuccess !== undefined) { ExportMetricsPartialSuccess.encode(message.partialSuccess, writer.uint32(10).fork()).ldelim(); } @@ -169,14 +180,19 @@ export const ExportMetricsServiceResponse = { return obj; }, - create, I>>(base?: I): ExportMetricsServiceResponse { + create, I>>( + base?: I + ): ExportMetricsServiceResponse { return ExportMetricsServiceResponse.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportMetricsServiceResponse { + fromPartial, I>>( + object: I + ): ExportMetricsServiceResponse { const message = createBaseExportMetricsServiceResponse(); - message.partialSuccess = (object.partialSuccess !== undefined && object.partialSuccess !== null) - ? ExportMetricsPartialSuccess.fromPartial(object.partialSuccess) - : undefined; + message.partialSuccess = + object.partialSuccess !== undefined && object.partialSuccess !== null + ? ExportMetricsPartialSuccess.fromPartial(object.partialSuccess) + : undefined; return message; }, }; @@ -186,10 +202,15 @@ function createBaseExportMetricsPartialSuccess(): ExportMetricsPartialSuccess { } export const ExportMetricsPartialSuccess = { - encode(message: ExportMetricsPartialSuccess, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExportMetricsPartialSuccess, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { if (message.rejectedDataPoints !== BigInt("0")) { if (BigInt.asIntN(64, message.rejectedDataPoints) !== message.rejectedDataPoints) { - throw new globalThis.Error("value provided for field message.rejectedDataPoints of type int64 too large"); + throw new globalThis.Error( + "value provided for field message.rejectedDataPoints of type int64 too large" + ); } writer.uint32(8).int64(message.rejectedDataPoints.toString()); } @@ -231,7 +252,9 @@ export const ExportMetricsPartialSuccess = { fromJSON(object: any): ExportMetricsPartialSuccess { return { - rejectedDataPoints: isSet(object.rejectedDataPoints) ? BigInt(object.rejectedDataPoints) : BigInt("0"), + rejectedDataPoints: isSet(object.rejectedDataPoints) + ? BigInt(object.rejectedDataPoints) + : BigInt("0"), errorMessage: isSet(object.errorMessage) ? globalThis.String(object.errorMessage) : "", }; }, @@ -247,10 +270,14 @@ export const ExportMetricsPartialSuccess = { return obj; }, - create, I>>(base?: I): ExportMetricsPartialSuccess { + create, I>>( + base?: I + ): ExportMetricsPartialSuccess { return ExportMetricsPartialSuccess.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportMetricsPartialSuccess { + fromPartial, I>>( + object: I + ): ExportMetricsPartialSuccess { const message = createBaseExportMetricsPartialSuccess(); message.rejectedDataPoints = object.rejectedDataPoints ?? BigInt("0"); message.errorMessage = object.errorMessage ?? ""; @@ -293,14 +320,19 @@ interface Rpc { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { 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 608e6c9127..ee38c623fa 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 @@ -108,10 +108,14 @@ export const ExportTraceServiceRequest = { return obj; }, - create, I>>(base?: I): ExportTraceServiceRequest { + create, I>>( + base?: I + ): ExportTraceServiceRequest { return ExportTraceServiceRequest.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportTraceServiceRequest { + fromPartial, I>>( + object: I + ): ExportTraceServiceRequest { const message = createBaseExportTraceServiceRequest(); message.resourceSpans = object.resourceSpans?.map((e) => ResourceSpans.fromPartial(e)) || []; return message; @@ -123,7 +127,10 @@ function createBaseExportTraceServiceResponse(): ExportTraceServiceResponse { } export const ExportTraceServiceResponse = { - encode(message: ExportTraceServiceResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExportTraceServiceResponse, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { if (message.partialSuccess !== undefined) { ExportTracePartialSuccess.encode(message.partialSuccess, writer.uint32(10).fork()).ldelim(); } @@ -169,14 +176,19 @@ export const ExportTraceServiceResponse = { return obj; }, - create, I>>(base?: I): ExportTraceServiceResponse { + create, I>>( + base?: I + ): ExportTraceServiceResponse { return ExportTraceServiceResponse.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportTraceServiceResponse { + fromPartial, I>>( + object: I + ): ExportTraceServiceResponse { const message = createBaseExportTraceServiceResponse(); - message.partialSuccess = (object.partialSuccess !== undefined && object.partialSuccess !== null) - ? ExportTracePartialSuccess.fromPartial(object.partialSuccess) - : undefined; + message.partialSuccess = + object.partialSuccess !== undefined && object.partialSuccess !== null + ? ExportTracePartialSuccess.fromPartial(object.partialSuccess) + : undefined; return message; }, }; @@ -189,7 +201,9 @@ export const ExportTracePartialSuccess = { encode(message: ExportTracePartialSuccess, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.rejectedSpans !== BigInt("0")) { if (BigInt.asIntN(64, message.rejectedSpans) !== message.rejectedSpans) { - throw new globalThis.Error("value provided for field message.rejectedSpans of type int64 too large"); + throw new globalThis.Error( + "value provided for field message.rejectedSpans of type int64 too large" + ); } writer.uint32(8).int64(message.rejectedSpans.toString()); } @@ -247,10 +261,14 @@ export const ExportTracePartialSuccess = { return obj; }, - create, I>>(base?: I): ExportTracePartialSuccess { + create, I>>( + base?: I + ): ExportTracePartialSuccess { return ExportTracePartialSuccess.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportTracePartialSuccess { + fromPartial, I>>( + object: I + ): ExportTracePartialSuccess { const message = createBaseExportTracePartialSuccess(); message.rejectedSpans = object.rejectedSpans ?? BigInt("0"); message.errorMessage = object.errorMessage ?? ""; @@ -293,14 +311,19 @@ interface Rpc { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { 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 7db2022e71..2a307a667c 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 @@ -93,7 +93,9 @@ export const AnyValue = { } if (message.intValue !== undefined) { if (BigInt.asIntN(64, message.intValue) !== message.intValue) { - throw new globalThis.Error("value provided for field message.intValue of type int64 too large"); + throw new globalThis.Error( + "value provided for field message.intValue of type int64 too large" + ); } writer.uint32(24).int64(message.intValue.toString()); } @@ -184,8 +186,12 @@ export const AnyValue = { intValue: isSet(object.intValue) ? BigInt(object.intValue) : undefined, doubleValue: isSet(object.doubleValue) ? globalThis.Number(object.doubleValue) : undefined, arrayValue: isSet(object.arrayValue) ? ArrayValue.fromJSON(object.arrayValue) : undefined, - kvlistValue: isSet(object.kvlistValue) ? KeyValueList.fromJSON(object.kvlistValue) : undefined, - bytesValue: isSet(object.bytesValue) ? Buffer.from(bytesFromBase64(object.bytesValue)) : undefined, + kvlistValue: isSet(object.kvlistValue) + ? KeyValueList.fromJSON(object.kvlistValue) + : undefined, + bytesValue: isSet(object.bytesValue) + ? Buffer.from(bytesFromBase64(object.bytesValue)) + : undefined, }; }, @@ -224,12 +230,14 @@ export const AnyValue = { message.boolValue = object.boolValue ?? undefined; message.intValue = object.intValue ?? undefined; message.doubleValue = object.doubleValue ?? undefined; - message.arrayValue = (object.arrayValue !== undefined && object.arrayValue !== null) - ? ArrayValue.fromPartial(object.arrayValue) - : undefined; - message.kvlistValue = (object.kvlistValue !== undefined && object.kvlistValue !== null) - ? KeyValueList.fromPartial(object.kvlistValue) - : undefined; + message.arrayValue = + object.arrayValue !== undefined && object.arrayValue !== null + ? ArrayValue.fromPartial(object.arrayValue) + : undefined; + message.kvlistValue = + object.kvlistValue !== undefined && object.kvlistValue !== null + ? KeyValueList.fromPartial(object.kvlistValue) + : undefined; message.bytesValue = object.bytesValue ?? undefined; return message; }, @@ -272,7 +280,9 @@ export const ArrayValue = { fromJSON(object: any): ArrayValue { return { - values: globalThis.Array.isArray(object?.values) ? object.values.map((e: any) => AnyValue.fromJSON(e)) : [], + values: globalThis.Array.isArray(object?.values) + ? object.values.map((e: any) => AnyValue.fromJSON(e)) + : [], }; }, @@ -331,7 +341,9 @@ export const KeyValueList = { fromJSON(object: any): KeyValueList { return { - values: globalThis.Array.isArray(object?.values) ? object.values.map((e: any) => KeyValue.fromJSON(e)) : [], + values: globalThis.Array.isArray(object?.values) + ? object.values.map((e: any) => KeyValue.fromJSON(e)) + : [], }; }, @@ -422,9 +434,10 @@ export const KeyValue = { fromPartial, I>>(object: I): KeyValue { const message = createBaseKeyValue(); message.key = object.key ?? ""; - message.value = (object.value !== undefined && object.value !== null) - ? AnyValue.fromPartial(object.value) - : undefined; + message.value = + object.value !== undefined && object.value !== null + ? AnyValue.fromPartial(object.value) + : undefined; return message; }, }; @@ -527,7 +540,9 @@ export const InstrumentationScope = { create, I>>(base?: I): InstrumentationScope { return InstrumentationScope.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): InstrumentationScope { + fromPartial, I>>( + object: I + ): InstrumentationScope { const message = createBaseInstrumentationScope(); message.name = object.name ?? ""; message.version = object.version ?? ""; @@ -564,14 +579,19 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { 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 503abbb7e0..2d0b3ebf56 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 @@ -255,9 +255,7 @@ export interface ResourceLogs { * The resource for the logs in this message. * If this field is not set then resource info is unknown. */ - resource: - | Resource - | undefined; + resource: Resource | undefined; /** A list of ScopeLogs that originate from a resource. */ scopeLogs: ScopeLogs[]; /** @@ -277,9 +275,7 @@ export interface ScopeLogs { * Semantically when InstrumentationScope isn't set, it is equivalent with * an empty instrumentation scope name (unknown). */ - scope: - | InstrumentationScope - | undefined; + scope: InstrumentationScope | undefined; /** A list of log records. */ logRecords: LogRecord[]; /** @@ -335,9 +331,7 @@ export interface LogRecord { * string message (including multi-line) describing the event in a free form or it can * be a structured data composed of arrays and maps of other values. [Optional]. */ - body: - | AnyValue - | undefined; + body: AnyValue | undefined; /** * Additional attributes that describe the specific event occurrence. [Optional]. * Attribute keys MUST be unique (it is not allowed to have more than one @@ -529,9 +523,10 @@ export const ResourceLogs = { }, fromPartial, I>>(object: I): ResourceLogs { const message = createBaseResourceLogs(); - message.resource = (object.resource !== undefined && object.resource !== null) - ? Resource.fromPartial(object.resource) - : undefined; + message.resource = + object.resource !== undefined && object.resource !== null + ? Resource.fromPartial(object.resource) + : undefined; message.scopeLogs = object.scopeLogs?.map((e) => ScopeLogs.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -622,9 +617,10 @@ export const ScopeLogs = { }, fromPartial, I>>(object: I): ScopeLogs { const message = createBaseScopeLogs(); - message.scope = (object.scope !== undefined && object.scope !== null) - ? InstrumentationScope.fromPartial(object.scope) - : undefined; + message.scope = + object.scope !== undefined && object.scope !== null + ? InstrumentationScope.fromPartial(object.scope) + : undefined; message.logRecords = object.logRecords?.map((e) => LogRecord.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -650,13 +646,17 @@ export const LogRecord = { encode(message: LogRecord, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(9).fixed64(message.timeUnixNano.toString()); } if (message.observedTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.observedTimeUnixNano) !== message.observedTimeUnixNano) { - throw new globalThis.Error("value provided for field message.observedTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.observedTimeUnixNano of type fixed64 too large" + ); } writer.uint32(89).fixed64(message.observedTimeUnixNano.toString()); } @@ -776,8 +776,12 @@ export const LogRecord = { fromJSON(object: any): LogRecord { return { timeUnixNano: isSet(object.timeUnixNano) ? BigInt(object.timeUnixNano) : BigInt("0"), - observedTimeUnixNano: isSet(object.observedTimeUnixNano) ? BigInt(object.observedTimeUnixNano) : BigInt("0"), - severityNumber: isSet(object.severityNumber) ? severityNumberFromJSON(object.severityNumber) : 0, + observedTimeUnixNano: isSet(object.observedTimeUnixNano) + ? BigInt(object.observedTimeUnixNano) + : BigInt("0"), + severityNumber: isSet(object.severityNumber) + ? severityNumberFromJSON(object.severityNumber) + : 0, severityText: isSet(object.severityText) ? globalThis.String(object.severityText) : "", body: isSet(object.body) ? AnyValue.fromJSON(object.body) : undefined, attributes: globalThis.Array.isArray(object?.attributes) @@ -787,7 +791,9 @@ export const LogRecord = { ? globalThis.Number(object.droppedAttributesCount) : 0, flags: isSet(object.flags) ? globalThis.Number(object.flags) : 0, - traceId: isSet(object.traceId) ? Buffer.from(bytesFromBase64(object.traceId)) : Buffer.alloc(0), + traceId: isSet(object.traceId) + ? Buffer.from(bytesFromBase64(object.traceId)) + : Buffer.alloc(0), spanId: isSet(object.spanId) ? Buffer.from(bytesFromBase64(object.spanId)) : Buffer.alloc(0), }; }, @@ -836,7 +842,10 @@ export const LogRecord = { message.observedTimeUnixNano = object.observedTimeUnixNano ?? BigInt("0"); message.severityNumber = object.severityNumber ?? 0; message.severityText = object.severityText ?? ""; - message.body = (object.body !== undefined && object.body !== null) ? AnyValue.fromPartial(object.body) : undefined; + message.body = + object.body !== undefined && object.body !== null + ? AnyValue.fromPartial(object.body) + : undefined; message.attributes = object.attributes?.map((e) => KeyValue.fromPartial(e)) || []; message.droppedAttributesCount = object.droppedAttributesCount ?? 0; message.flags = object.flags ?? 0; @@ -873,14 +882,19 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { 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 a09922a4ec..0f368d5487 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 @@ -193,9 +193,7 @@ export interface ResourceMetrics { * The resource for the metrics in this message. * If this field is not set then no resource info is known. */ - resource: - | Resource - | undefined; + resource: Resource | undefined; /** A list of metrics that originate from a resource. */ scopeMetrics: ScopeMetrics[]; /** @@ -215,9 +213,7 @@ export interface ScopeMetrics { * Semantically when InstrumentationScope isn't set, it is equivalent with * an empty instrumentation scope name (unknown). */ - scope: - | InstrumentationScope - | undefined; + scope: InstrumentationScope | undefined; /** A list of metrics that originate from an instrumentation library. */ metrics: Metric[]; /** @@ -329,9 +325,7 @@ export interface Metric { sum?: Sum | undefined; histogram?: Histogram | undefined; exponentialHistogram?: ExponentialHistogram | undefined; - summary?: - | Summary - | undefined; + summary?: Summary | undefined; /** * Additional metadata attributes that describe the metric. [Optional]. * Attributes are non-identifying. @@ -440,9 +434,7 @@ export interface NumberDataPoint { */ timeUnixNano: bigint; asDouble?: number | undefined; - asInt?: - | bigint - | undefined; + asInt?: bigint | undefined; /** * (Optional) List of exemplars collected from * measurements that were used to form the data point @@ -506,9 +498,7 @@ export interface HistogramDataPoint { * doing so. This is specifically to enforce compatibility w/ OpenMetrics, * see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram */ - sum?: - | number - | undefined; + sum?: number | undefined; /** * bucket_counts is an optional field contains the count values of histogram * for each bucket. @@ -546,9 +536,7 @@ export interface HistogramDataPoint { */ flags: number; /** min is the minimum value over (start_time, end_time]. */ - min?: - | number - | undefined; + min?: number | undefined; /** max is the maximum value over (start_time, end_time]. */ max?: number | undefined; } @@ -598,9 +586,7 @@ export interface ExponentialHistogramDataPoint { * doing so. This is specifically to enforce compatibility w/ OpenMetrics, * see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram */ - sum?: - | number - | undefined; + sum?: number | undefined; /** * scale describes the resolution of the histogram. Boundaries are * located at powers of the base, where: @@ -631,13 +617,9 @@ export interface ExponentialHistogramDataPoint { */ zeroCount: bigint; /** positive carries the positive range of exponential bucket counts. */ - positive: - | ExponentialHistogramDataPoint_Buckets - | undefined; + positive: ExponentialHistogramDataPoint_Buckets | undefined; /** negative carries the negative range of exponential bucket counts. */ - negative: - | ExponentialHistogramDataPoint_Buckets - | undefined; + negative: ExponentialHistogramDataPoint_Buckets | undefined; /** * Flags that apply to this specific data point. See DataPointFlags * for the available flags and their meaning. @@ -649,13 +631,9 @@ export interface ExponentialHistogramDataPoint { */ exemplars: Exemplar[]; /** min is the minimum value over (start_time, end_time]. */ - min?: - | number - | undefined; + min?: number | undefined; /** max is the maximum value over (start_time, end_time]. */ - max?: - | number - | undefined; + max?: number | undefined; /** * ZeroThreshold may be optionally set to convey the width of the zero * region. Where the zero region is defined as the closed interval @@ -789,9 +767,7 @@ export interface Exemplar { */ timeUnixNano: bigint; asDouble?: number | undefined; - asInt?: - | bigint - | undefined; + asInt?: bigint | undefined; /** * (Optional) Span ID of the exemplar trace. * span_id may be missing if the measurement is not recorded inside a trace @@ -862,7 +838,8 @@ export const MetricsData = { }, fromPartial, I>>(object: I): MetricsData { const message = createBaseMetricsData(); - message.resourceMetrics = object.resourceMetrics?.map((e) => ResourceMetrics.fromPartial(e)) || []; + message.resourceMetrics = + object.resourceMetrics?.map((e) => ResourceMetrics.fromPartial(e)) || []; return message; }, }; @@ -951,9 +928,10 @@ export const ResourceMetrics = { }, fromPartial, I>>(object: I): ResourceMetrics { const message = createBaseResourceMetrics(); - message.resource = (object.resource !== undefined && object.resource !== null) - ? Resource.fromPartial(object.resource) - : undefined; + message.resource = + object.resource !== undefined && object.resource !== null + ? Resource.fromPartial(object.resource) + : undefined; message.scopeMetrics = object.scopeMetrics?.map((e) => ScopeMetrics.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -1018,7 +996,9 @@ export const ScopeMetrics = { fromJSON(object: any): ScopeMetrics { return { scope: isSet(object.scope) ? InstrumentationScope.fromJSON(object.scope) : undefined, - metrics: globalThis.Array.isArray(object?.metrics) ? object.metrics.map((e: any) => Metric.fromJSON(e)) : [], + metrics: globalThis.Array.isArray(object?.metrics) + ? object.metrics.map((e: any) => Metric.fromJSON(e)) + : [], schemaUrl: isSet(object.schemaUrl) ? globalThis.String(object.schemaUrl) : "", }; }, @@ -1042,9 +1022,10 @@ export const ScopeMetrics = { }, fromPartial, I>>(object: I): ScopeMetrics { const message = createBaseScopeMetrics(); - message.scope = (object.scope !== undefined && object.scope !== null) - ? InstrumentationScope.fromPartial(object.scope) - : undefined; + message.scope = + object.scope !== undefined && object.scope !== null + ? InstrumentationScope.fromPartial(object.scope) + : undefined; message.metrics = object.metrics?.map((e) => Metric.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -1188,7 +1169,9 @@ export const Metric = { ? ExponentialHistogram.fromJSON(object.exponentialHistogram) : undefined, summary: isSet(object.summary) ? Summary.fromJSON(object.summary) : undefined, - metadata: globalThis.Array.isArray(object?.metadata) ? object.metadata.map((e: any) => KeyValue.fromJSON(e)) : [], + metadata: globalThis.Array.isArray(object?.metadata) + ? object.metadata.map((e: any) => KeyValue.fromJSON(e)) + : [], }; }, @@ -1232,17 +1215,24 @@ export const Metric = { message.name = object.name ?? ""; message.description = object.description ?? ""; message.unit = object.unit ?? ""; - message.gauge = (object.gauge !== undefined && object.gauge !== null) ? Gauge.fromPartial(object.gauge) : undefined; - message.sum = (object.sum !== undefined && object.sum !== null) ? Sum.fromPartial(object.sum) : undefined; - message.histogram = (object.histogram !== undefined && object.histogram !== null) - ? Histogram.fromPartial(object.histogram) - : undefined; - message.exponentialHistogram = (object.exponentialHistogram !== undefined && object.exponentialHistogram !== null) - ? ExponentialHistogram.fromPartial(object.exponentialHistogram) - : undefined; - message.summary = (object.summary !== undefined && object.summary !== null) - ? Summary.fromPartial(object.summary) - : undefined; + message.gauge = + object.gauge !== undefined && object.gauge !== null + ? Gauge.fromPartial(object.gauge) + : undefined; + message.sum = + object.sum !== undefined && object.sum !== null ? Sum.fromPartial(object.sum) : undefined; + message.histogram = + object.histogram !== undefined && object.histogram !== null + ? Histogram.fromPartial(object.histogram) + : undefined; + message.exponentialHistogram = + object.exponentialHistogram !== undefined && object.exponentialHistogram !== null + ? ExponentialHistogram.fromPartial(object.exponentialHistogram) + : undefined; + message.summary = + object.summary !== undefined && object.summary !== null + ? Summary.fromPartial(object.summary) + : undefined; message.metadata = object.metadata?.map((e) => KeyValue.fromPartial(e)) || []; return message; }, @@ -1550,9 +1540,12 @@ export const ExponentialHistogram = { create, I>>(base?: I): ExponentialHistogram { return ExponentialHistogram.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExponentialHistogram { + fromPartial, I>>( + object: I + ): ExponentialHistogram { const message = createBaseExponentialHistogram(); - message.dataPoints = object.dataPoints?.map((e) => ExponentialHistogramDataPoint.fromPartial(e)) || []; + message.dataPoints = + object.dataPoints?.map((e) => ExponentialHistogramDataPoint.fromPartial(e)) || []; message.aggregationTemporality = object.aggregationTemporality ?? 0; return message; }, @@ -1638,13 +1631,17 @@ export const NumberDataPoint = { } if (message.startTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.startTimeUnixNano) !== message.startTimeUnixNano) { - throw new globalThis.Error("value provided for field message.startTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.startTimeUnixNano of type fixed64 too large" + ); } writer.uint32(17).fixed64(message.startTimeUnixNano.toString()); } if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(25).fixed64(message.timeUnixNano.toString()); } @@ -1653,7 +1650,9 @@ export const NumberDataPoint = { } if (message.asInt !== undefined) { if (BigInt.asIntN(64, message.asInt) !== message.asInt) { - throw new globalThis.Error("value provided for field message.asInt of type sfixed64 too large"); + throw new globalThis.Error( + "value provided for field message.asInt of type sfixed64 too large" + ); } writer.uint32(49).sfixed64(message.asInt.toString()); } @@ -1736,7 +1735,9 @@ export const NumberDataPoint = { attributes: globalThis.Array.isArray(object?.attributes) ? object.attributes.map((e: any) => KeyValue.fromJSON(e)) : [], - startTimeUnixNano: isSet(object.startTimeUnixNano) ? BigInt(object.startTimeUnixNano) : BigInt("0"), + startTimeUnixNano: isSet(object.startTimeUnixNano) + ? BigInt(object.startTimeUnixNano) + : BigInt("0"), timeUnixNano: isSet(object.timeUnixNano) ? BigInt(object.timeUnixNano) : BigInt("0"), asDouble: isSet(object.asDouble) ? globalThis.Number(object.asDouble) : undefined, asInt: isSet(object.asInt) ? BigInt(object.asInt) : undefined, @@ -1812,19 +1813,25 @@ export const HistogramDataPoint = { } if (message.startTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.startTimeUnixNano) !== message.startTimeUnixNano) { - throw new globalThis.Error("value provided for field message.startTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.startTimeUnixNano of type fixed64 too large" + ); } writer.uint32(17).fixed64(message.startTimeUnixNano.toString()); } if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(25).fixed64(message.timeUnixNano.toString()); } if (message.count !== BigInt("0")) { if (BigInt.asUintN(64, message.count) !== message.count) { - throw new globalThis.Error("value provided for field message.count of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.count of type fixed64 too large" + ); } writer.uint32(33).fixed64(message.count.toString()); } @@ -1834,7 +1841,9 @@ export const HistogramDataPoint = { writer.uint32(50).fork(); for (const v of message.bucketCounts) { if (BigInt.asUintN(64, v) !== v) { - throw new globalThis.Error("a value provided in array field bucketCounts of type fixed64 is too large"); + throw new globalThis.Error( + "a value provided in array field bucketCounts of type fixed64 is too large" + ); } writer.fixed64(v.toString()); } @@ -1977,7 +1986,9 @@ export const HistogramDataPoint = { attributes: globalThis.Array.isArray(object?.attributes) ? object.attributes.map((e: any) => KeyValue.fromJSON(e)) : [], - startTimeUnixNano: isSet(object.startTimeUnixNano) ? BigInt(object.startTimeUnixNano) : BigInt("0"), + startTimeUnixNano: isSet(object.startTimeUnixNano) + ? BigInt(object.startTimeUnixNano) + : BigInt("0"), timeUnixNano: isSet(object.timeUnixNano) ? BigInt(object.timeUnixNano) : BigInt("0"), count: isSet(object.count) ? BigInt(object.count) : BigInt("0"), sum: isSet(object.sum) ? globalThis.Number(object.sum) : undefined, @@ -2074,25 +2085,34 @@ function createBaseExponentialHistogramDataPoint(): ExponentialHistogramDataPoin } export const ExponentialHistogramDataPoint = { - encode(message: ExponentialHistogramDataPoint, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExponentialHistogramDataPoint, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { for (const v of message.attributes) { KeyValue.encode(v!, writer.uint32(10).fork()).ldelim(); } if (message.startTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.startTimeUnixNano) !== message.startTimeUnixNano) { - throw new globalThis.Error("value provided for field message.startTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.startTimeUnixNano of type fixed64 too large" + ); } writer.uint32(17).fixed64(message.startTimeUnixNano.toString()); } if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(25).fixed64(message.timeUnixNano.toString()); } if (message.count !== BigInt("0")) { if (BigInt.asUintN(64, message.count) !== message.count) { - throw new globalThis.Error("value provided for field message.count of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.count of type fixed64 too large" + ); } writer.uint32(33).fixed64(message.count.toString()); } @@ -2104,15 +2124,23 @@ export const ExponentialHistogramDataPoint = { } if (message.zeroCount !== BigInt("0")) { if (BigInt.asUintN(64, message.zeroCount) !== message.zeroCount) { - throw new globalThis.Error("value provided for field message.zeroCount of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.zeroCount of type fixed64 too large" + ); } writer.uint32(57).fixed64(message.zeroCount.toString()); } if (message.positive !== undefined) { - ExponentialHistogramDataPoint_Buckets.encode(message.positive, writer.uint32(66).fork()).ldelim(); + ExponentialHistogramDataPoint_Buckets.encode( + message.positive, + writer.uint32(66).fork() + ).ldelim(); } if (message.negative !== undefined) { - ExponentialHistogramDataPoint_Buckets.encode(message.negative, writer.uint32(74).fork()).ldelim(); + ExponentialHistogramDataPoint_Buckets.encode( + message.negative, + writer.uint32(74).fork() + ).ldelim(); } if (message.flags !== 0) { writer.uint32(80).uint32(message.flags); @@ -2251,14 +2279,20 @@ export const ExponentialHistogramDataPoint = { attributes: globalThis.Array.isArray(object?.attributes) ? object.attributes.map((e: any) => KeyValue.fromJSON(e)) : [], - startTimeUnixNano: isSet(object.startTimeUnixNano) ? BigInt(object.startTimeUnixNano) : BigInt("0"), + startTimeUnixNano: isSet(object.startTimeUnixNano) + ? BigInt(object.startTimeUnixNano) + : BigInt("0"), timeUnixNano: isSet(object.timeUnixNano) ? BigInt(object.timeUnixNano) : BigInt("0"), count: isSet(object.count) ? BigInt(object.count) : BigInt("0"), sum: isSet(object.sum) ? globalThis.Number(object.sum) : undefined, scale: isSet(object.scale) ? globalThis.Number(object.scale) : 0, zeroCount: isSet(object.zeroCount) ? BigInt(object.zeroCount) : BigInt("0"), - positive: isSet(object.positive) ? ExponentialHistogramDataPoint_Buckets.fromJSON(object.positive) : undefined, - negative: isSet(object.negative) ? ExponentialHistogramDataPoint_Buckets.fromJSON(object.negative) : undefined, + positive: isSet(object.positive) + ? ExponentialHistogramDataPoint_Buckets.fromJSON(object.positive) + : undefined, + negative: isSet(object.negative) + ? ExponentialHistogramDataPoint_Buckets.fromJSON(object.negative) + : undefined, flags: isSet(object.flags) ? globalThis.Number(object.flags) : 0, exemplars: globalThis.Array.isArray(object?.exemplars) ? object.exemplars.map((e: any) => Exemplar.fromJSON(e)) @@ -2316,11 +2350,13 @@ export const ExponentialHistogramDataPoint = { return obj; }, - create, I>>(base?: I): ExponentialHistogramDataPoint { + create, I>>( + base?: I + ): ExponentialHistogramDataPoint { return ExponentialHistogramDataPoint.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( - object: I, + object: I ): ExponentialHistogramDataPoint { const message = createBaseExponentialHistogramDataPoint(); message.attributes = object.attributes?.map((e) => KeyValue.fromPartial(e)) || []; @@ -2330,12 +2366,14 @@ export const ExponentialHistogramDataPoint = { message.sum = object.sum ?? undefined; message.scale = object.scale ?? 0; message.zeroCount = object.zeroCount ?? BigInt("0"); - message.positive = (object.positive !== undefined && object.positive !== null) - ? ExponentialHistogramDataPoint_Buckets.fromPartial(object.positive) - : undefined; - message.negative = (object.negative !== undefined && object.negative !== null) - ? ExponentialHistogramDataPoint_Buckets.fromPartial(object.negative) - : undefined; + message.positive = + object.positive !== undefined && object.positive !== null + ? ExponentialHistogramDataPoint_Buckets.fromPartial(object.positive) + : undefined; + message.negative = + object.negative !== undefined && object.negative !== null + ? ExponentialHistogramDataPoint_Buckets.fromPartial(object.negative) + : undefined; message.flags = object.flags ?? 0; message.exemplars = object.exemplars?.map((e) => Exemplar.fromPartial(e)) || []; message.min = object.min ?? undefined; @@ -2350,14 +2388,19 @@ function createBaseExponentialHistogramDataPoint_Buckets(): ExponentialHistogram } export const ExponentialHistogramDataPoint_Buckets = { - encode(message: ExponentialHistogramDataPoint_Buckets, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExponentialHistogramDataPoint_Buckets, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { if (message.offset !== 0) { writer.uint32(8).sint32(message.offset); } writer.uint32(18).fork(); for (const v of message.bucketCounts) { if (BigInt.asUintN(64, v) !== v) { - throw new globalThis.Error("a value provided in array field bucketCounts of type uint64 is too large"); + throw new globalThis.Error( + "a value provided in array field bucketCounts of type uint64 is too large" + ); } writer.uint64(v.toString()); } @@ -2426,12 +2469,12 @@ export const ExponentialHistogramDataPoint_Buckets = { }, create, I>>( - base?: I, + base?: I ): ExponentialHistogramDataPoint_Buckets { return ExponentialHistogramDataPoint_Buckets.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( - object: I, + object: I ): ExponentialHistogramDataPoint_Buckets { const message = createBaseExponentialHistogramDataPoint_Buckets(); message.offset = object.offset ?? 0; @@ -2459,19 +2502,25 @@ export const SummaryDataPoint = { } if (message.startTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.startTimeUnixNano) !== message.startTimeUnixNano) { - throw new globalThis.Error("value provided for field message.startTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.startTimeUnixNano of type fixed64 too large" + ); } writer.uint32(17).fixed64(message.startTimeUnixNano.toString()); } if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(25).fixed64(message.timeUnixNano.toString()); } if (message.count !== BigInt("0")) { if (BigInt.asUintN(64, message.count) !== message.count) { - throw new globalThis.Error("value provided for field message.count of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.count of type fixed64 too large" + ); } writer.uint32(33).fixed64(message.count.toString()); } @@ -2534,7 +2583,9 @@ export const SummaryDataPoint = { break; } - message.quantileValues.push(SummaryDataPoint_ValueAtQuantile.decode(reader, reader.uint32())); + message.quantileValues.push( + SummaryDataPoint_ValueAtQuantile.decode(reader, reader.uint32()) + ); continue; case 8: if (tag !== 64) { @@ -2557,7 +2608,9 @@ export const SummaryDataPoint = { attributes: globalThis.Array.isArray(object?.attributes) ? object.attributes.map((e: any) => KeyValue.fromJSON(e)) : [], - startTimeUnixNano: isSet(object.startTimeUnixNano) ? BigInt(object.startTimeUnixNano) : BigInt("0"), + startTimeUnixNano: isSet(object.startTimeUnixNano) + ? BigInt(object.startTimeUnixNano) + : BigInt("0"), timeUnixNano: isSet(object.timeUnixNano) ? BigInt(object.timeUnixNano) : BigInt("0"), count: isSet(object.count) ? BigInt(object.count) : BigInt("0"), sum: isSet(object.sum) ? globalThis.Number(object.sum) : 0, @@ -2586,7 +2639,9 @@ export const SummaryDataPoint = { obj.sum = message.sum; } if (message.quantileValues?.length) { - obj.quantileValues = message.quantileValues.map((e) => SummaryDataPoint_ValueAtQuantile.toJSON(e)); + obj.quantileValues = message.quantileValues.map((e) => + SummaryDataPoint_ValueAtQuantile.toJSON(e) + ); } if (message.flags !== 0) { obj.flags = Math.round(message.flags); @@ -2604,7 +2659,8 @@ export const SummaryDataPoint = { message.timeUnixNano = object.timeUnixNano ?? BigInt("0"); message.count = object.count ?? BigInt("0"); message.sum = object.sum ?? 0; - message.quantileValues = object.quantileValues?.map((e) => SummaryDataPoint_ValueAtQuantile.fromPartial(e)) || []; + message.quantileValues = + object.quantileValues?.map((e) => SummaryDataPoint_ValueAtQuantile.fromPartial(e)) || []; message.flags = object.flags ?? 0; return message; }, @@ -2615,7 +2671,10 @@ function createBaseSummaryDataPoint_ValueAtQuantile(): SummaryDataPoint_ValueAtQ } export const SummaryDataPoint_ValueAtQuantile = { - encode(message: SummaryDataPoint_ValueAtQuantile, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: SummaryDataPoint_ValueAtQuantile, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { if (message.quantile !== 0) { writer.uint32(9).double(message.quantile); } @@ -2674,12 +2733,12 @@ export const SummaryDataPoint_ValueAtQuantile = { }, create, I>>( - base?: I, + base?: I ): SummaryDataPoint_ValueAtQuantile { return SummaryDataPoint_ValueAtQuantile.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( - object: I, + object: I ): SummaryDataPoint_ValueAtQuantile { const message = createBaseSummaryDataPoint_ValueAtQuantile(); message.quantile = object.quantile ?? 0; @@ -2706,7 +2765,9 @@ export const Exemplar = { } if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(17).fixed64(message.timeUnixNano.toString()); } @@ -2715,7 +2776,9 @@ export const Exemplar = { } if (message.asInt !== undefined) { if (BigInt.asIntN(64, message.asInt) !== message.asInt) { - throw new globalThis.Error("value provided for field message.asInt of type sfixed64 too large"); + throw new globalThis.Error( + "value provided for field message.asInt of type sfixed64 too large" + ); } writer.uint32(49).sfixed64(message.asInt.toString()); } @@ -2795,7 +2858,9 @@ export const Exemplar = { asDouble: isSet(object.asDouble) ? globalThis.Number(object.asDouble) : undefined, asInt: isSet(object.asInt) ? BigInt(object.asInt) : undefined, spanId: isSet(object.spanId) ? Buffer.from(bytesFromBase64(object.spanId)) : Buffer.alloc(0), - traceId: isSet(object.traceId) ? Buffer.from(bytesFromBase64(object.traceId)) : Buffer.alloc(0), + traceId: isSet(object.traceId) + ? Buffer.from(bytesFromBase64(object.traceId)) + : Buffer.alloc(0), }; }, @@ -2827,7 +2892,8 @@ export const Exemplar = { }, fromPartial, I>>(object: I): Exemplar { const message = createBaseExemplar(); - message.filteredAttributes = object.filteredAttributes?.map((e) => KeyValue.fromPartial(e)) || []; + message.filteredAttributes = + object.filteredAttributes?.map((e) => KeyValue.fromPartial(e)) || []; message.timeUnixNano = object.timeUnixNano ?? BigInt("0"); message.asDouble = object.asDouble ?? undefined; message.asInt = object.asInt ?? undefined; @@ -2864,14 +2930,19 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { 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 b634f23134..60ac223995 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 @@ -99,14 +99,19 @@ export const Resource = { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function isSet(value: any): boolean { 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 835205809b..b90afa3c2c 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 @@ -106,9 +106,7 @@ export interface ResourceSpans { * The resource for the spans in this message. * If this field is not set then no resource info is known. */ - resource: - | Resource - | undefined; + resource: Resource | undefined; /** A list of ScopeSpans that originate from a resource. */ scopeSpans: ScopeSpans[]; /** @@ -128,9 +126,7 @@ export interface ScopeSpans { * Semantically when InstrumentationScope isn't set, it is equivalent with * an empty instrumentation scope name (unknown). */ - scope: - | InstrumentationScope - | undefined; + scope: InstrumentationScope | undefined; /** A list of Spans that originate from an instrumentation scope. */ spans: Span[]; /** @@ -648,9 +644,10 @@ export const ResourceSpans = { }, fromPartial, I>>(object: I): ResourceSpans { const message = createBaseResourceSpans(); - message.resource = (object.resource !== undefined && object.resource !== null) - ? Resource.fromPartial(object.resource) - : undefined; + message.resource = + object.resource !== undefined && object.resource !== null + ? Resource.fromPartial(object.resource) + : undefined; message.scopeSpans = object.scopeSpans?.map((e) => ScopeSpans.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -715,7 +712,9 @@ export const ScopeSpans = { fromJSON(object: any): ScopeSpans { return { scope: isSet(object.scope) ? InstrumentationScope.fromJSON(object.scope) : undefined, - spans: globalThis.Array.isArray(object?.spans) ? object.spans.map((e: any) => Span.fromJSON(e)) : [], + spans: globalThis.Array.isArray(object?.spans) + ? object.spans.map((e: any) => Span.fromJSON(e)) + : [], schemaUrl: isSet(object.schemaUrl) ? globalThis.String(object.schemaUrl) : "", }; }, @@ -739,9 +738,10 @@ export const ScopeSpans = { }, fromPartial, I>>(object: I): ScopeSpans { const message = createBaseScopeSpans(); - message.scope = (object.scope !== undefined && object.scope !== null) - ? InstrumentationScope.fromPartial(object.scope) - : undefined; + message.scope = + object.scope !== undefined && object.scope !== null + ? InstrumentationScope.fromPartial(object.scope) + : undefined; message.spans = object.spans?.map((e) => Span.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -794,13 +794,17 @@ export const Span = { } if (message.startTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.startTimeUnixNano) !== message.startTimeUnixNano) { - throw new globalThis.Error("value provided for field message.startTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.startTimeUnixNano of type fixed64 too large" + ); } writer.uint32(57).fixed64(message.startTimeUnixNano.toString()); } if (message.endTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.endTimeUnixNano) !== message.endTimeUnixNano) { - throw new globalThis.Error("value provided for field message.endTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.endTimeUnixNano of type fixed64 too large" + ); } writer.uint32(65).fixed64(message.endTimeUnixNano.toString()); } @@ -958,14 +962,20 @@ export const Span = { fromJSON(object: any): Span { return { - traceId: isSet(object.traceId) ? Buffer.from(bytesFromBase64(object.traceId)) : Buffer.alloc(0), + traceId: isSet(object.traceId) + ? Buffer.from(bytesFromBase64(object.traceId)) + : Buffer.alloc(0), spanId: isSet(object.spanId) ? Buffer.from(bytesFromBase64(object.spanId)) : Buffer.alloc(0), traceState: isSet(object.traceState) ? globalThis.String(object.traceState) : "", - parentSpanId: isSet(object.parentSpanId) ? Buffer.from(bytesFromBase64(object.parentSpanId)) : Buffer.alloc(0), + parentSpanId: isSet(object.parentSpanId) + ? Buffer.from(bytesFromBase64(object.parentSpanId)) + : Buffer.alloc(0), flags: isSet(object.flags) ? globalThis.Number(object.flags) : 0, name: isSet(object.name) ? globalThis.String(object.name) : "", kind: isSet(object.kind) ? span_SpanKindFromJSON(object.kind) : 0, - startTimeUnixNano: isSet(object.startTimeUnixNano) ? BigInt(object.startTimeUnixNano) : BigInt("0"), + startTimeUnixNano: isSet(object.startTimeUnixNano) + ? BigInt(object.startTimeUnixNano) + : BigInt("0"), endTimeUnixNano: isSet(object.endTimeUnixNano) ? BigInt(object.endTimeUnixNano) : BigInt("0"), attributes: globalThis.Array.isArray(object?.attributes) ? object.attributes.map((e: any) => KeyValue.fromJSON(e)) @@ -973,10 +983,18 @@ export const Span = { droppedAttributesCount: isSet(object.droppedAttributesCount) ? globalThis.Number(object.droppedAttributesCount) : 0, - events: globalThis.Array.isArray(object?.events) ? object.events.map((e: any) => Span_Event.fromJSON(e)) : [], - droppedEventsCount: isSet(object.droppedEventsCount) ? globalThis.Number(object.droppedEventsCount) : 0, - links: globalThis.Array.isArray(object?.links) ? object.links.map((e: any) => Span_Link.fromJSON(e)) : [], - droppedLinksCount: isSet(object.droppedLinksCount) ? globalThis.Number(object.droppedLinksCount) : 0, + events: globalThis.Array.isArray(object?.events) + ? object.events.map((e: any) => Span_Event.fromJSON(e)) + : [], + droppedEventsCount: isSet(object.droppedEventsCount) + ? globalThis.Number(object.droppedEventsCount) + : 0, + links: globalThis.Array.isArray(object?.links) + ? object.links.map((e: any) => Span_Link.fromJSON(e)) + : [], + droppedLinksCount: isSet(object.droppedLinksCount) + ? globalThis.Number(object.droppedLinksCount) + : 0, status: isSet(object.status) ? Status.fromJSON(object.status) : undefined, }; }, @@ -1054,9 +1072,10 @@ export const Span = { message.droppedEventsCount = object.droppedEventsCount ?? 0; message.links = object.links?.map((e) => Span_Link.fromPartial(e)) || []; message.droppedLinksCount = object.droppedLinksCount ?? 0; - message.status = (object.status !== undefined && object.status !== null) - ? Status.fromPartial(object.status) - : undefined; + message.status = + object.status !== undefined && object.status !== null + ? Status.fromPartial(object.status) + : undefined; return message; }, }; @@ -1069,7 +1088,9 @@ export const Span_Event = { encode(message: Span_Event, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(9).fixed64(message.timeUnixNano.toString()); } @@ -1266,7 +1287,9 @@ export const Span_Link = { fromJSON(object: any): Span_Link { return { - traceId: isSet(object.traceId) ? Buffer.from(bytesFromBase64(object.traceId)) : Buffer.alloc(0), + traceId: isSet(object.traceId) + ? Buffer.from(bytesFromBase64(object.traceId)) + : Buffer.alloc(0), spanId: isSet(object.spanId) ? Buffer.from(bytesFromBase64(object.spanId)) : Buffer.alloc(0), traceState: isSet(object.traceState) ? globalThis.String(object.traceState) : "", attributes: globalThis.Array.isArray(object?.attributes) @@ -1418,14 +1441,19 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { diff --git a/internal-packages/rbac/src/ability.test.ts b/internal-packages/rbac/src/ability.test.ts index e361bf0d54..a3250bd003 100644 --- a/internal-packages/rbac/src/ability.test.ts +++ b/internal-packages/rbac/src/ability.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect } from "vitest"; -import { permissiveAbility, superAbility, denyAbility, buildFallbackAbility, buildJwtAbility } from "./ability.js"; +import { + permissiveAbility, + superAbility, + denyAbility, + buildFallbackAbility, + buildJwtAbility, +} from "./ability.js"; describe("permissiveAbility", () => { it("allows any action on any resource type", () => { diff --git a/internal-packages/rbac/src/fallback.ts b/internal-packages/rbac/src/fallback.ts index eed301e7d8..7ada941f75 100644 --- a/internal-packages/rbac/src/fallback.ts +++ b/internal-packages/rbac/src/fallback.ts @@ -91,7 +91,10 @@ class RoleBaseAccessFallbackController implements RoleBaseAccessController { // pre-RBAC routes that haven't been migrated, but it's a dead // code path for any route that uses `createLoaderApiRoute` / // `createActionApiRoute`. - const rawToken = request.headers.get("Authorization")?.replace(/^Bearer /, "").trim(); + const rawToken = request.headers + .get("Authorization") + ?.replace(/^Bearer /, "") + .trim(); if (!rawToken) return { ok: false, status: 401, error: "Invalid or Missing API key" }; if (options?.allowJWT && isPublicJWT(rawToken)) { @@ -165,9 +168,7 @@ class RoleBaseAccessFallbackController implements RoleBaseAccessController { }, }, parentEnvironment: { select: { id: true, apiKey: true } }, - childEnvironments: branchName - ? { where: { branchName, archivedAt: null } } - : undefined, + childEnvironments: branchName ? { where: { branchName, archivedAt: null } } : undefined, } as const; let env = await this.replica.runtimeEnvironment.findFirst({ where: { apiKey: rawToken }, @@ -338,7 +339,10 @@ class RoleBaseAccessFallbackController implements RoleBaseAccessController { request: Request, context: { organizationId?: string; projectId?: string } ): Promise { - const rawToken = request.headers.get("Authorization")?.replace(/^Bearer /, "").trim(); + const rawToken = request.headers + .get("Authorization") + ?.replace(/^Bearer /, "") + .trim(); if (!rawToken || !isUserActorToken(rawToken)) { return { ok: false, status: 401, error: "Invalid or Missing user-actor token" }; } @@ -431,7 +435,9 @@ function isPublicJWT(token: string): boolean { const parts = token.split("."); if (parts.length !== 3) return false; try { - const payload = JSON.parse(Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8")); + const payload = JSON.parse( + Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8") + ); return payload !== null && typeof payload === "object" && payload.pub === true; } catch { return false; @@ -442,7 +448,9 @@ function extractJWTSub(token: string): string | undefined { const parts = token.split("."); if (parts.length !== 3) return undefined; try { - const payload = JSON.parse(Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8")); + const payload = JSON.parse( + Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8") + ); return payload !== null && typeof payload === "object" && typeof payload.sub === "string" ? payload.sub : undefined; diff --git a/internal-packages/rbac/src/index.ts b/internal-packages/rbac/src/index.ts index 7dbd9f83d1..258dc12f3b 100644 --- a/internal-packages/rbac/src/index.ts +++ b/internal-packages/rbac/src/index.ts @@ -108,10 +108,8 @@ class LazyController implements RoleBaseAccessController { // specifier in the error message is the plugin's own moduleName. const code = (err as NodeJS.ErrnoException | undefined)?.code; const message = err instanceof Error ? err.message : String(err); - const isModuleNotFound = - code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; - const isPluginItselfMissing = - isModuleNotFound && message.includes(moduleName); + const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; + const isPluginItselfMissing = isModuleNotFound && message.includes(moduleName); if (!isPluginItselfMissing) { // Either the error wasn't a missing-module error at all, or the @@ -122,9 +120,7 @@ class LazyController implements RoleBaseAccessController { err ); } else if (process.env.RBAC_LOG_FALLBACK === "1") { - console.log( - "RBAC: no plugin installed (ERR_MODULE_NOT_FOUND); using fallback" - ); + console.log("RBAC: no plugin installed (ERR_MODULE_NOT_FOUND); using fallback"); } // Fail-fast for deployments that require plugins to be present. Set @@ -135,9 +131,7 @@ class LazyController implements RoleBaseAccessController { // rolled back. Self-hosters leave REQUIRE_PLUGINS unset and continue // to use the fallback when no plugin is installed. if (process.env.REQUIRE_PLUGINS === "1") { - throw new Error( - `REQUIRE_PLUGINS=1 but plugin "${moduleName}" did not load: ${message}` - ); + throw new Error(`REQUIRE_PLUGINS=1 but plugin "${moduleName}" did not load: ${message}`); } return new RoleBaseAccessFallback(prisma, { @@ -289,10 +283,7 @@ class LazyController implements RoleBaseAccessController { class RoleBaseAccess { // Synchronous β€” returns a lazy controller that resolves any installed // plugin on first call. - create( - prisma: RbacPrismaInput, - options?: RbacCreateOptions - ): RoleBaseAccessController { + create(prisma: RbacPrismaInput, options?: RbacCreateOptions): RoleBaseAccessController { return new LazyController(prisma, options); } } diff --git a/internal-packages/redis/package.json b/internal-packages/redis/package.json index 6c7d8aa260..14cf7f33d6 100644 --- a/internal-packages/redis/package.json +++ b/internal-packages/redis/package.json @@ -12,4 +12,4 @@ "scripts": { "typecheck": "tsc --noEmit" } -} \ No newline at end of file +} diff --git a/internal-packages/redis/src/index.ts b/internal-packages/redis/src/index.ts index a1283d6153..6fb30b9a4b 100644 --- a/internal-packages/redis/src/index.ts +++ b/internal-packages/redis/src/index.ts @@ -24,11 +24,7 @@ export { Redis, type Callback, type RedisOptions, type Result, type RedisCommand */ export function defaultReconnectOnError(err: Error): boolean | 1 | 2 { const msg = err.message ?? ""; - if ( - msg.startsWith("READONLY") || - msg.startsWith("LOADING") || - msg.startsWith("UNBLOCKED") - ) { + if (msg.startsWith("READONLY") || msg.startsWith("LOADING") || msg.startsWith("UNBLOCKED")) { return 2; } return false; diff --git a/internal-packages/replication/package.json b/internal-packages/replication/package.json index 3aedabb3ed..626aa42fed 100644 --- a/internal-packages/replication/package.json +++ b/internal-packages/replication/package.json @@ -25,4 +25,4 @@ "test": "vitest --sequence.concurrent=false --no-file-parallelism", "test:coverage": "vitest --sequence.concurrent=false --no-file-parallelism --coverage.enabled" } -} \ No newline at end of file +} diff --git a/internal-packages/run-engine/package.json b/internal-packages/run-engine/package.json index 414452da3b..8d53974d10 100644 --- a/internal-packages/run-engine/package.json +++ b/internal-packages/run-engine/package.json @@ -48,4 +48,4 @@ "build": "pnpm run clean && tsc -p tsconfig.build.json", "dev": "tsc --watch -p tsconfig.build.json" } -} \ No newline at end of file +} diff --git a/internal-packages/run-engine/src/batch-queue/completionTracker.ts b/internal-packages/run-engine/src/batch-queue/completionTracker.ts index 05793002fe..f6570cfc54 100644 --- a/internal-packages/run-engine/src/batch-queue/completionTracker.ts +++ b/internal-packages/run-engine/src/batch-queue/completionTracker.ts @@ -45,9 +45,9 @@ export class BatchCompletionTracker { }) { this.redis = createRedisClient(options.redis); this.logger = options.logger ?? { - debug: () => { }, - info: () => { }, - error: () => { }, + debug: () => {}, + info: () => {}, + error: () => {}, }; this.#registerCommands(); diff --git a/internal-packages/run-engine/src/batch-queue/index.ts b/internal-packages/run-engine/src/batch-queue/index.ts index 1f7a6221f4..10354e77c5 100644 --- a/internal-packages/run-engine/src/batch-queue/index.ts +++ b/internal-packages/run-engine/src/batch-queue/index.ts @@ -186,17 +186,17 @@ export class BatchQueue { // so we don't need the DLQ - we just need the retry scheduling. ...(options.retry ? { - retry: { - strategy: new ExponentialBackoffRetry({ - maxAttempts: options.retry.maxAttempts, - minTimeoutInMs: options.retry.minTimeoutInMs ?? 1_000, - maxTimeoutInMs: options.retry.maxTimeoutInMs ?? 30_000, - factor: options.retry.factor ?? 2, - randomize: options.retry.randomize ?? true, - }), - deadLetterQueue: false, - }, - } + retry: { + strategy: new ExponentialBackoffRetry({ + maxAttempts: options.retry.maxAttempts, + minTimeoutInMs: options.retry.minTimeoutInMs ?? 1_000, + maxTimeoutInMs: options.retry.maxTimeoutInMs ?? 30_000, + factor: options.retry.factor ?? 2, + randomize: options.retry.randomize ?? true, + }), + deadLetterQueue: false, + }, + } : {}), logger: this.logger, tracer: options.tracer, @@ -887,11 +887,7 @@ export class BatchQueue { }); await this.#startSpan("BatchQueue.failMessage", async () => { - return this.fairQueue.failMessage( - messageId, - queueId, - new Error(result.error) - ); + return this.fairQueue.failMessage(messageId, queueId, new Error(result.error)); }); // Don't record failure or check completion - message will be retried diff --git a/internal-packages/run-engine/src/batch-queue/tests/index.test.ts b/internal-packages/run-engine/src/batch-queue/tests/index.test.ts index ac44dae708..56386b32ab 100644 --- a/internal-packages/run-engine/src/batch-queue/tests/index.test.ts +++ b/internal-packages/run-engine/src/batch-queue/tests/index.test.ts @@ -717,64 +717,61 @@ describe("BatchQueue", () => { } ); - redisTest( - "should delay processing when rate limited", - async ({ redisContainer }) => { - let limitCallCount = 0; - const rateLimiter: GlobalRateLimiter = { - async limit() { - limitCallCount++; - // Rate limit the first 3 calls, then allow - if (limitCallCount <= 3) { - return { allowed: false, resetAt: Date.now() + 100 }; - } - return { allowed: true }; - }, - }; + redisTest("should delay processing when rate limited", async ({ redisContainer }) => { + let limitCallCount = 0; + const rateLimiter: GlobalRateLimiter = { + async limit() { + limitCallCount++; + // Rate limit the first 3 calls, then allow + if (limitCallCount <= 3) { + return { allowed: false, resetAt: Date.now() + 100 }; + } + return { allowed: true }; + }, + }; - const queue = new BatchQueue({ - redis: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - keyPrefix: "test:", - }, - drr: { quantum: 5, maxDeficit: 50 }, - consumerCount: 1, - consumerIntervalMs: 50, - startConsumers: true, - globalRateLimiter: rateLimiter, - }); + const queue = new BatchQueue({ + redis: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + keyPrefix: "test:", + }, + drr: { quantum: 5, maxDeficit: 50 }, + consumerCount: 1, + consumerIntervalMs: 50, + startConsumers: true, + globalRateLimiter: rateLimiter, + }); - let completionResult: CompleteBatchResult | null = null; + let completionResult: CompleteBatchResult | null = null; - try { - queue.onProcessItem(async ({ itemIndex }) => { - return { success: true, runId: `run_${itemIndex}` }; - }); + try { + queue.onProcessItem(async ({ itemIndex }) => { + return { success: true, runId: `run_${itemIndex}` }; + }); - queue.onBatchComplete(async (result) => { - completionResult = result; - }); + queue.onBatchComplete(async (result) => { + completionResult = result; + }); - await queue.initializeBatch(createInitOptions("batch1", "env1", 3)); - await enqueueItems(queue, "batch1", "env1", createBatchItems(3)); + await queue.initializeBatch(createInitOptions("batch1", "env1", 3)); + await enqueueItems(queue, "batch1", "env1", createBatchItems(3)); - // Should still complete despite initial rate limiting - await vi.waitFor( - () => { - expect(completionResult).not.toBeNull(); - }, - { timeout: 10000 } - ); + // Should still complete despite initial rate limiting + await vi.waitFor( + () => { + expect(completionResult).not.toBeNull(); + }, + { timeout: 10000 } + ); - expect(completionResult!.successfulRunCount).toBe(3); - // Rate limiter was called more times than items due to initial rejections - expect(limitCallCount).toBeGreaterThan(3); - } finally { - await queue.close(); - } + expect(completionResult!.successfulRunCount).toBe(3); + // Rate limiter was called more times than items due to initial rejections + expect(limitCallCount).toBeGreaterThan(3); + } finally { + await queue.close(); } - ); + }); }); describe("skipRetries on failed items", () => { diff --git a/internal-packages/run-engine/src/engine/index.ts b/internal-packages/run-engine/src/engine/index.ts index 5a733e0c1c..106f5947fe 100644 --- a/internal-packages/run-engine/src/engine/index.ts +++ b/internal-packages/run-engine/src/engine/index.ts @@ -126,7 +126,10 @@ export class RunEngine { this.logger = options.logger ?? new Logger("RunEngine", this.options.logLevel ?? "info"); this.prisma = options.prisma; this.readOnlyPrisma = options.readOnlyPrisma ?? this.prisma; - this.runStore = new PostgresRunStore({ prisma: this.prisma, readOnlyPrisma: this.readOnlyPrisma }); + this.runStore = new PostgresRunStore({ + prisma: this.prisma, + readOnlyPrisma: this.readOnlyPrisma, + }); this.runLockRedis = createRedisClient( { ...options.runLock.redis, @@ -205,14 +208,14 @@ export class RunEngine { ttlSystem: options.queue?.ttlSystem?.disabled ? undefined : { - shardCount: options.queue?.ttlSystem?.shardCount, - pollIntervalMs: options.queue?.ttlSystem?.pollIntervalMs, - batchSize: options.queue?.ttlSystem?.batchSize, - consumersDisabled: options.queue?.ttlSystem?.consumersDisabled, - workerQueueSuffix: "ttl-worker:{queue:ttl-expiration:}queue", - workerItemsSuffix: "ttl-worker:{queue:ttl-expiration:}items", - visibilityTimeoutMs: options.queue?.ttlSystem?.visibilityTimeoutMs ?? 30_000, - }, + shardCount: options.queue?.ttlSystem?.shardCount, + pollIntervalMs: options.queue?.ttlSystem?.pollIntervalMs, + batchSize: options.queue?.ttlSystem?.batchSize, + consumersDisabled: options.queue?.ttlSystem?.consumersDisabled, + workerQueueSuffix: "ttl-worker:{queue:ttl-expiration:}queue", + workerItemsSuffix: "ttl-worker:{queue:ttl-expiration:}items", + visibilityTimeoutMs: options.queue?.ttlSystem?.visibilityTimeoutMs ?? 30_000, + }, }); this.worker = new Worker({ @@ -233,9 +236,9 @@ export class RunEngine { id: payload.waitpointId, output: payload.error ? { - value: payload.error, - isError: true, - } + value: payload.error, + isError: true, + } : undefined, }); }, @@ -408,7 +411,11 @@ export class RunEngine { // Start TTL worker whenever TTL system is enabled, so expired runs enqueued by the // Lua script get processed even when the main engine worker is disabled (e.g. in tests). - if (options.queue?.ttlSystem && !options.queue.ttlSystem.disabled && !options.queue.ttlSystem.consumersDisabled) { + if ( + options.queue?.ttlSystem && + !options.queue.ttlSystem.disabled && + !options.queue.ttlSystem.consumersDisabled + ) { this.ttlWorker.start(); } @@ -529,14 +536,13 @@ export class RunEngine { const intervalMs = this.options.workerQueueObserver.intervalMs ?? 30_000; this.workerQueueObserverAbortController = new AbortController(); - this.#runWorkerQueueObserver( - intervalMs, - this.workerQueueObserverAbortController.signal - ).catch((error) => { - this.logger.error("Worker queue observer loop crashed", { - error: error instanceof Error ? error.message : String(error), - }); - }); + this.#runWorkerQueueObserver(intervalMs, this.workerQueueObserverAbortController.signal).catch( + (error) => { + this.logger.error("Worker queue observer loop crashed", { + error: error instanceof Error ? error.message : String(error), + }); + } + ); } async #runWorkerQueueObserver(intervalMs: number, signal: AbortSignal) { @@ -617,7 +623,7 @@ export class RunEngine { */ emitRunCancelledEvent?: boolean; }, - tx?: PrismaClientOrTransaction, + tx?: PrismaClientOrTransaction ): Promise { const prisma = tx ?? this.prisma; return startSpan(this.tracer, "createCancelledRun", async (span) => { @@ -665,9 +671,10 @@ export class RunEngine { // will be an empty object, which Prisma misreads as a relation // update op. Normalise to a real array (or undefined for the // empty case). - runTags: Array.isArray(snapshot.tags) && snapshot.tags.length > 0 - ? snapshot.tags - : undefined, + runTags: + Array.isArray(snapshot.tags) && snapshot.tags.length > 0 + ? snapshot.tags + : undefined, oneTimeUseToken: snapshot.oneTimeUseToken, parentTaskRunId: snapshot.parentTaskRunId, rootTaskRunId: snapshot.rootTaskRunId, @@ -733,13 +740,10 @@ export class RunEngine { // P2002 = unique constraint violation. Double-pop after a drainer // requeue can reach this. Idempotent: return the existing row // without re-emitting. - if ( - err instanceof Prisma.PrismaClientKnownRequestError && - err.code === "P2002" - ) { + if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") { this.logger.info( "createCancelledRun: row already exists, returning existing (idempotent)", - { friendlyId: snapshot.friendlyId }, + { friendlyId: snapshot.friendlyId } ); const existing = await this.runStore.findRun({ id }, prisma); if (existing) { @@ -754,7 +758,7 @@ export class RunEngine { return existing; } throw new Error( - `createCancelledRun conflict: existing run ${snapshot.friendlyId} has status ${existing.status}`, + `createCancelledRun conflict: existing run ${snapshot.friendlyId} has status ${existing.status}` ); } } @@ -843,18 +847,18 @@ export class RunEngine { debounce: debounce.mode === "trailing" ? { - ...debounce, - updateData: { - payload, - payloadType, - metadata, - metadataType, - tags, - maxAttempts, - maxDurationInSeconds, - machine, - }, - } + ...debounce, + updateData: { + payload, + payloadType, + metadata, + metadataType, + tags, + maxAttempts, + maxDurationInSeconds, + machine, + }, + } : debounce, tx: prisma, }); @@ -993,10 +997,10 @@ export class RunEngine { streamBasinName, debounce: debounce ? { - key: debounce.key, - delay: debounce.delay, - createdAt: new Date(), - } + key: debounce.key, + delay: debounce.delay, + createdAt: new Date(), + } : undefined, annotations, }, @@ -1017,9 +1021,9 @@ export class RunEngine { associatedWaitpoint: resumeParentOnCompletion && parentTaskRunId ? this.waitpointSystem.buildRunAssociatedWaitpoint({ - projectId: environment.project.id, - environmentId: environment.id, - }) + projectId: environment.project.id, + environmentId: environment.id, + }) : undefined, }, prisma @@ -1048,9 +1052,7 @@ export class RunEngine { }); if (targetFields.includes("oneTimeUseToken")) { - throw new RunOneTimeUseTokenError( - `One-time use token has already been used` - ); + throw new RunOneTimeUseTokenError(`One-time use token has already been used`); } // Only idempotency key collisions should be retried @@ -1260,9 +1262,9 @@ export class RunEngine { const waitpointData = resumeParentOnCompletion && parentTaskRunId ? this.waitpointSystem.buildRunAssociatedWaitpoint({ - projectId: environment.project.id, - environmentId: environment.id, - }) + projectId: environment.project.id, + environmentId: environment.id, + }) : undefined; // Create the run in terminal SYSTEM_FAILURE status. @@ -1307,11 +1309,7 @@ export class RunEngine { // If parent is waiting, block it with the waitpoint then immediately // complete it with the error output so the parent can resume. - if ( - resumeParentOnCompletion && - parentTaskRunId && - taskRun.associatedWaitpoint - ) { + if (resumeParentOnCompletion && parentTaskRunId && taskRun.associatedWaitpoint) { await this.waitpointSystem.blockRunAndCompleteWaitpoint({ runId: parentTaskRunId, waitpointId: taskRun.associatedWaitpoint.id, @@ -1579,7 +1577,10 @@ export class RunEngine { return this.runQueue.lengthOfEnvQueue(environment); } - async lengthOfQueue(environment: MinimalAuthenticatedEnvironment, queue: string): Promise { + async lengthOfQueue( + environment: MinimalAuthenticatedEnvironment, + queue: string + ): Promise { return this.runQueue.lengthOfQueue(environment, queue); } @@ -2499,21 +2500,21 @@ export class RunEngine { const error = latestSnapshot.environmentType === "DEVELOPMENT" ? ({ - type: "INTERNAL_ERROR", - code: taskStalledErrorCode, - message: errorMessage, - } satisfies TaskRunInternalError) - : this.options.treatProductionExecutionStallsAsOOM - ? ({ - type: "INTERNAL_ERROR", - code: "TASK_PROCESS_OOM_KILLED", - message: "Run was terminated due to running out of memory", - } satisfies TaskRunInternalError) - : ({ type: "INTERNAL_ERROR", code: taskStalledErrorCode, message: errorMessage, - } satisfies TaskRunInternalError); + } satisfies TaskRunInternalError) + : this.options.treatProductionExecutionStallsAsOOM + ? ({ + type: "INTERNAL_ERROR", + code: "TASK_PROCESS_OOM_KILLED", + message: "Run was terminated due to running out of memory", + } satisfies TaskRunInternalError) + : ({ + type: "INTERNAL_ERROR", + code: taskStalledErrorCode, + message: errorMessage, + } satisfies TaskRunInternalError); await this.runAttemptSystem.attemptFailed({ runId, @@ -2524,10 +2525,10 @@ export class RunEngine { error, retry: shouldRetry ? { - //250ms in the future - timestamp: Date.now() + retryDelay, - delay: retryDelay, - } + //250ms in the future + timestamp: Date.now() + retryDelay, + delay: retryDelay, + } : undefined, }, forceRequeue: true, diff --git a/internal-packages/run-engine/src/engine/retrying.ts b/internal-packages/run-engine/src/engine/retrying.ts index a64dfb796e..a4a6eaf2f1 100644 --- a/internal-packages/run-engine/src/engine/retrying.ts +++ b/internal-packages/run-engine/src/engine/retrying.ts @@ -186,13 +186,16 @@ async function retryOOMOnMachine( prisma: PrismaClientOrTransaction, runStore: RunStore, runId: string -): Promise<{ - machine: string; - retrySettings: RetryOptions; - usageDurationMs: number; - costInCents: number; - machinePreset: string | null; -} | undefined> { +): Promise< + | { + machine: string; + retrySettings: RetryOptions; + usageDurationMs: number; + costInCents: number; + machinePreset: string | null; + } + | undefined +> { try { const run = await runStore.findRun( { diff --git a/internal-packages/run-engine/src/engine/systems/delayedRunSystem.ts b/internal-packages/run-engine/src/engine/systems/delayedRunSystem.ts index a77e60d05e..cd22895429 100644 --- a/internal-packages/run-engine/src/engine/systems/delayedRunSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/delayedRunSystem.ts @@ -203,7 +203,6 @@ export class DelayedRunSystem { id: run.runtimeEnvironmentId, }, }); - }); } diff --git a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts index d8e9656f39..38d1cf79a8 100644 --- a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts @@ -87,7 +87,9 @@ function enhanceExecutionSnapshotWithWaitpoints( type: w.type, completedAt: w.completedAt ?? new Date(), idempotencyKey: - w.userProvidedIdempotencyKey && !w.inactiveIdempotencyKey ? w.idempotencyKey : undefined, + w.userProvidedIdempotencyKey && !w.inactiveIdempotencyKey + ? w.idempotencyKey + : undefined, completedByTaskRun: w.completedByTaskRunId ? { id: w.completedByTaskRunId, diff --git a/internal-packages/run-engine/src/engine/systems/pendingVersionSystem.ts b/internal-packages/run-engine/src/engine/systems/pendingVersionSystem.ts index 741ad8a14f..281808d751 100644 --- a/internal-packages/run-engine/src/engine/systems/pendingVersionSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/pendingVersionSystem.ts @@ -73,8 +73,8 @@ export class PendingVersionSystem { // Step 1: ask the injected lookup (typically ClickHouse-backed) for // candidate run ids. Best-effort β€” results may be stale or incomplete. - const { runIds: candidateIds } = await this.$.pendingVersionRunIdLookup - .lookupPendingVersionRunIds({ + const { runIds: candidateIds } = + await this.$.pendingVersionRunIdLookup.lookupPendingVersionRunIds({ organizationId: backgroundWorker.runtimeEnvironment.organizationId, projectId: backgroundWorker.projectId, environmentId: backgroundWorker.runtimeEnvironmentId, diff --git a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts index 977c94a8e8..cb733919cb 100644 --- a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts @@ -1407,7 +1407,7 @@ export class RunAttemptSystem { const run = await this.$.runStore.cancelRun( runId, { - completedAt: finalizeRun ? completedAt ?? new Date() : completedAt, + completedAt: finalizeRun ? (completedAt ?? new Date()) : completedAt, error, ...(bulkActionId && { bulkActionId }), ...(usageUpdate && { @@ -2012,14 +2012,11 @@ export class RunAttemptSystem { if (!metadata.success) { // Customer's metadata operations don't match the schema (typically // non-JSON values in `operations[].value`). System ignores it. - this.$.logger.warn( - "RunEngine.completeRunAttempt(): failed to validate flushed metadata", - { - runId, - flushedMetadata: completion.flushedMetadata, - error: metadata.error, - } - ); + this.$.logger.warn("RunEngine.completeRunAttempt(): failed to validate flushed metadata", { + runId, + flushedMetadata: completion.flushedMetadata, + error: metadata.error, + }); return; } diff --git a/internal-packages/run-engine/src/engine/systems/ttlSystem.ts b/internal-packages/run-engine/src/engine/systems/ttlSystem.ts index faffa2c59e..ac5950b884 100644 --- a/internal-packages/run-engine/src/engine/systems/ttlSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/ttlSystem.ts @@ -157,136 +157,132 @@ export class TtlSystem { expired: string[]; skipped: { runId: string; reason: string }[]; }> { - return startSpan( - this.$.tracer, - "TtlSystem.expireRunsBatch", - async (span) => { - span.setAttribute("runCount", runIds.length); - - if (runIds.length === 0) { - return { expired: [], skipped: [] }; - } - - const expired: string[] = []; - const skipped: { runId: string; reason: string }[] = []; + return startSpan(this.$.tracer, "TtlSystem.expireRunsBatch", async (span) => { + span.setAttribute("runCount", runIds.length); - // Fetch all runs in a single query (no snapshot data needed) - const runs = await this.$.runStore.findRuns({ - where: { id: { in: runIds } }, - select: { - id: true, - spanId: true, - status: true, - lockedAt: true, - ttl: true, - taskEventStore: true, - createdAt: true, - associatedWaitpoint: { select: { id: true } }, - organizationId: true, - projectId: true, - runtimeEnvironmentId: true, - }, - }); - - // Filter runs that can be expired - const runsToExpire: typeof runs = []; + if (runIds.length === 0) { + return { expired: [], skipped: [] }; + } - for (const run of runs) { - if (run.status !== "PENDING") { - skipped.push({ runId: run.id, reason: `status_${run.status}` }); - continue; - } + const expired: string[] = []; + const skipped: { runId: string; reason: string }[] = []; + + // Fetch all runs in a single query (no snapshot data needed) + const runs = await this.$.runStore.findRuns({ + where: { id: { in: runIds } }, + select: { + id: true, + spanId: true, + status: true, + lockedAt: true, + ttl: true, + taskEventStore: true, + createdAt: true, + associatedWaitpoint: { select: { id: true } }, + organizationId: true, + projectId: true, + runtimeEnvironmentId: true, + }, + }); - if (run.lockedAt) { - skipped.push({ runId: run.id, reason: "locked" }); - continue; - } + // Filter runs that can be expired + const runsToExpire: typeof runs = []; - runsToExpire.push(run); + for (const run of runs) { + if (run.status !== "PENDING") { + skipped.push({ runId: run.id, reason: `status_${run.status}` }); + continue; } - // Track runs that weren't found - const foundRunIds = new Set(runs.map((r) => r.id)); - for (const runId of runIds) { - if (!foundRunIds.has(runId)) { - skipped.push({ runId, reason: "not_found" }); - } + if (run.lockedAt) { + skipped.push({ runId: run.id, reason: "locked" }); + continue; } - if (runsToExpire.length === 0) { - span.setAttribute("expiredCount", 0); - span.setAttribute("skippedCount", skipped.length); - return { expired, skipped }; + runsToExpire.push(run); + } + + // Track runs that weren't found + const foundRunIds = new Set(runs.map((r) => r.id)); + for (const runId of runIds) { + if (!foundRunIds.has(runId)) { + skipped.push({ runId, reason: "not_found" }); } + } - // Update all runs in a single SQL call (status, dates, and error JSON) - const now = new Date(); - const runIdsToExpire = runsToExpire.map((r) => r.id); - - const error: TaskRunError = { - type: "STRING_ERROR", - raw: "Run expired because the TTL was reached", - }; - - await this.$.runStore.expireRunsBatch(runIdsToExpire, { error, now }, this.$.prisma); - - // Process each run: enqueue waitpoint completion jobs and emit events - await pMap( - runsToExpire, - async (run) => { - try { - // Enqueue a finishWaitpoint worker job for resilient waitpoint completion - if (run.associatedWaitpoint) { - await this.$.worker.enqueue({ - id: `finishWaitpoint.ttl.${run.associatedWaitpoint.id}`, - job: "finishWaitpoint", - payload: { - waitpointId: run.associatedWaitpoint.id, - error: JSON.stringify(error), - }, - }); - } - - // This should really never happen - if (!run.organizationId) { - return; - } - - // Emit event - this.$.eventBus.emit("runExpired", { - run: { - id: run.id, - spanId: run.spanId, - ttl: run.ttl, - taskEventStore: run.taskEventStore, - createdAt: run.createdAt, - updatedAt: now, - completedAt: now, - expiredAt: now, - status: "EXPIRED" as TaskRunStatus, + if (runsToExpire.length === 0) { + span.setAttribute("expiredCount", 0); + span.setAttribute("skippedCount", skipped.length); + return { expired, skipped }; + } + + // Update all runs in a single SQL call (status, dates, and error JSON) + const now = new Date(); + const runIdsToExpire = runsToExpire.map((r) => r.id); + + const error: TaskRunError = { + type: "STRING_ERROR", + raw: "Run expired because the TTL was reached", + }; + + await this.$.runStore.expireRunsBatch(runIdsToExpire, { error, now }, this.$.prisma); + + // Process each run: enqueue waitpoint completion jobs and emit events + await pMap( + runsToExpire, + async (run) => { + try { + // Enqueue a finishWaitpoint worker job for resilient waitpoint completion + if (run.associatedWaitpoint) { + await this.$.worker.enqueue({ + id: `finishWaitpoint.ttl.${run.associatedWaitpoint.id}`, + job: "finishWaitpoint", + payload: { + waitpointId: run.associatedWaitpoint.id, + error: JSON.stringify(error), }, - time: now, - organization: { id: run.organizationId }, - project: { id: run.projectId }, - environment: { id: run.runtimeEnvironmentId }, }); + } - expired.push(run.id); - } catch (e) { - this.$.logger.error("Failed to process expired run", { - runId: run.id, - error: e, - }); + // This should really never happen + if (!run.organizationId) { + return; } - }, - { concurrency: 10, stopOnError: false } - ); - span.setAttribute("expiredCount", expired.length); - span.setAttribute("skippedCount", skipped.length); + // Emit event + this.$.eventBus.emit("runExpired", { + run: { + id: run.id, + spanId: run.spanId, + ttl: run.ttl, + taskEventStore: run.taskEventStore, + createdAt: run.createdAt, + updatedAt: now, + completedAt: now, + expiredAt: now, + status: "EXPIRED" as TaskRunStatus, + }, + time: now, + organization: { id: run.organizationId }, + project: { id: run.projectId }, + environment: { id: run.runtimeEnvironmentId }, + }); + + expired.push(run.id); + } catch (e) { + this.$.logger.error("Failed to process expired run", { + runId: run.id, + error: e, + }); + } + }, + { concurrency: 10, stopOnError: false } + ); - return { expired, skipped }; - } - ); + span.setAttribute("expiredCount", expired.length); + span.setAttribute("skippedCount", skipped.length); + + return { expired, skipped }; + }); } } diff --git a/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts b/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts index 29eba297be..85e1334ef4 100644 --- a/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts @@ -450,7 +450,7 @@ export class WaitpointSystem { // isolation, each statement gets its own snapshot. The CTE's snapshot is taken when // it starts, so if a concurrent completeWaitpoint commits during the CTE, the CTE // won't see it. This fresh query gets a new snapshot that reflects the latest commits. - const pendingCheck = await prisma.$queryRaw<{ pending_count: BigInt }[]>` + const pendingCheck = await prisma.$queryRaw<{ pending_count: bigint }[]>` SELECT COUNT(*) as pending_count FROM "Waitpoint" WHERE id IN (${Prisma.join($waitpoints)}) diff --git a/internal-packages/run-engine/src/engine/tests/batchTriggerAndWait.test.ts b/internal-packages/run-engine/src/engine/tests/batchTriggerAndWait.test.ts index a632c70739..26ec7b4f0d 100644 --- a/internal-packages/run-engine/src/engine/tests/batchTriggerAndWait.test.ts +++ b/internal-packages/run-engine/src/engine/tests/batchTriggerAndWait.test.ts @@ -985,8 +985,8 @@ describe("RunEngine batchTriggerAndWait", () => { result.failedRunCount > 0 && result.successfulRunCount === 0 ? "ABORTED" : result.failedRunCount > 0 - ? "PARTIAL_FAILED" - : "PENDING"; + ? "PARTIAL_FAILED" + : "PENDING"; // Update batch in database await prisma.batchTaskRun.update({ diff --git a/internal-packages/run-engine/src/engine/tests/cancelling.test.ts b/internal-packages/run-engine/src/engine/tests/cancelling.test.ts index aecae7a263..55f52aed17 100644 --- a/internal-packages/run-engine/src/engine/tests/cancelling.test.ts +++ b/internal-packages/run-engine/src/engine/tests/cancelling.test.ts @@ -217,9 +217,8 @@ describe("RunEngine cancelling", () => { expect(childEvent.run.spanId).toBe(childRun.spanId); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); } finally { await engine.quit(); @@ -313,9 +312,8 @@ describe("RunEngine cancelling", () => { expect(parentEvent.run.spanId).toBe(parentRun.spanId); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); } finally { await engine.quit(); @@ -440,9 +438,8 @@ describe("RunEngine cancelling", () => { expect(parentEvent.run.spanId).toBe(parentRun.spanId); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); } finally { await engine.quit(); diff --git a/internal-packages/run-engine/src/engine/tests/createCancelledRun.test.ts b/internal-packages/run-engine/src/engine/tests/createCancelledRun.test.ts index 68662074ea..40e9717955 100644 --- a/internal-packages/run-engine/src/engine/tests/createCancelledRun.test.ts +++ b/internal-packages/run-engine/src/engine/tests/createCancelledRun.test.ts @@ -97,52 +97,49 @@ describe("RunEngine.createCancelledRun", () => { } finally { await engine.quit(); } - }, + } ); - containerTest( - "emits runCancelled with correct payload", - async ({ prisma, redisOptions }) => { - const env = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - const engine = new RunEngine({ prisma, ...baseEngineOptions(redisOptions) }); - const captured: EventBusEventArgs<"runCancelled">[0][] = []; - engine.eventBus.on("runCancelled", (event) => { - captured.push(event); - }); + containerTest("emits runCancelled with correct payload", async ({ prisma, redisOptions }) => { + const env = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + const engine = new RunEngine({ prisma, ...baseEngineOptions(redisOptions) }); + const captured: EventBusEventArgs<"runCancelled">[0][] = []; + engine.eventBus.on("runCancelled", (event) => { + captured.push(event); + }); - try { - const cancelledAt = new Date(); - const cancelReason = "Test cancel"; - const friendlyId = freshRunId(); - await engine.createCancelledRun({ - snapshot: { - friendlyId, - environment: env, - taskIdentifier: "test-task", - payload: "{}", - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "0000000000000000cccc000000000000", - spanId: "dddd000000000000", - queue: "task/test-task", - isTest: false, - tags: [], - }, - cancelledAt, - cancelReason, - }); + try { + const cancelledAt = new Date(); + const cancelReason = "Test cancel"; + const friendlyId = freshRunId(); + await engine.createCancelledRun({ + snapshot: { + friendlyId, + environment: env, + taskIdentifier: "test-task", + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "0000000000000000cccc000000000000", + spanId: "dddd000000000000", + queue: "task/test-task", + isTest: false, + tags: [], + }, + cancelledAt, + cancelReason, + }); - expect(captured).toHaveLength(1); - expect(captured[0]!.run.status).toBe("CANCELED"); - expect(captured[0]!.run.friendlyId).toBe(friendlyId); - expect(captured[0]!.run.error).toEqual({ type: "STRING_ERROR", raw: cancelReason }); - expect(captured[0]!.organization.id).toBe(env.organization.id); - } finally { - await engine.quit(); - } - }, - ); + expect(captured).toHaveLength(1); + expect(captured[0]!.run.status).toBe("CANCELED"); + expect(captured[0]!.run.friendlyId).toBe(friendlyId); + expect(captured[0]!.run.error).toEqual({ type: "STRING_ERROR", raw: cancelReason }); + expect(captured[0]!.organization.id).toBe(env.organization.id); + } finally { + await engine.quit(); + } + }); containerTest( "emitRunCancelledEvent: false suppresses the bus emit but still writes the CANCELED PG row", @@ -192,7 +189,7 @@ describe("RunEngine.createCancelledRun", () => { } finally { await engine.quit(); } - }, + } ); containerTest( @@ -232,7 +229,7 @@ describe("RunEngine.createCancelledRun", () => { } finally { await engine.quit(); } - }, + } ); // Regression: cjson encodes empty Lua tables as `{}`, not `[]`. When @@ -279,7 +276,7 @@ describe("RunEngine.createCancelledRun", () => { } finally { await engine.quit(); } - }, + } ); // Regression: the P2002-on-id idempotency path used to return ANY @@ -335,11 +332,11 @@ describe("RunEngine.createCancelledRun", () => { }, cancelledAt: new Date(), cancelReason: "Should not silently overwrite a live row", - }), + }) ).rejects.toThrow(/createCancelledRun conflict.*PENDING/); } finally { await engine.quit(); } - }, + } ); }); diff --git a/internal-packages/run-engine/src/engine/tests/createFailedTaskRun.test.ts b/internal-packages/run-engine/src/engine/tests/createFailedTaskRun.test.ts index 84d33baa87..a6d2eba2a7 100644 --- a/internal-packages/run-engine/src/engine/tests/createFailedTaskRun.test.ts +++ b/internal-packages/run-engine/src/engine/tests/createFailedTaskRun.test.ts @@ -9,105 +9,108 @@ import { setupAuthenticatedEnvironment } from "./setup.js"; vi.setConfig({ testTimeout: 60_000 }); describe("RunEngine.createFailedTaskRun", () => { - containerTest("emits runFailed so the alert pipeline wakes up", async ({ prisma, redisOptions }) => { - // The mollifier drainer (and batch-trigger over-limit path) call - // createFailedTaskRun to write a terminal SYSTEM_FAILURE PG row - // for runs that never actually executed. Without an explicit - // runFailed emit, the row lands silently β€” the - // runEngineHandlers' `runFailed` listener (which enqueues - // PerformTaskRunAlertsService) never fires, so customers' - // configured TASK_RUN alert channels miss the failure entirely. - // - // Regression intent: if the emit is removed or moved out of - // createFailedTaskRun's success path, this test fails. The - // shape assertions pin the fields the alert delivery service - // reads from the event payload (run.id, run.status, error, - // attemptNumber=0 as the never-ran-marker). - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest( + "emits runFailed so the alert pipeline wakes up", + async ({ prisma, redisOptions }) => { + // The mollifier drainer (and batch-trigger over-limit path) call + // createFailedTaskRun to write a terminal SYSTEM_FAILURE PG row + // for runs that never actually executed. Without an explicit + // runFailed emit, the row lands silently β€” the + // runEngineHandlers' `runFailed` listener (which enqueues + // PerformTaskRunAlertsService) never fires, so customers' + // configured TASK_RUN alert channels miss the failure entirely. + // + // Regression intent: if the emit is removed or moved out of + // createFailedTaskRun's success path, this test fails. The + // shape assertions pin the fields the alert delivery service + // reads from the event payload (run.id, run.status, error, + // attemptNumber=0 as the never-ran-marker). + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - redis: redisOptions, - masterQueueConsumersDisabled: true, - processWorkerQueueDebounceMs: 50, - }, - runLock: { - redis: redisOptions, - }, - machines: { - defaultMachine: "small-1x", + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + masterQueueConsumersDisabled: true, + processWorkerQueueDebounceMs: 50, + }, + runLock: { + redis: redisOptions, + }, machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, + defaultMachine: "small-1x", + machines: { + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, + }, }, + baseCostInCents: 0.0005, }, - baseCostInCents: 0.0005, - }, - tracer: trace.getTracer("test", "0.0.0"), - }); - - try { - const failedEvents: EventBusEventArgs<"runFailed">[0][] = []; - engine.eventBus.on("runFailed", (event) => { - failedEvents.push(event); + tracer: trace.getTracer("test", "0.0.0"), }); - const friendlyId = generateFriendlyId("run"); - const taskIdentifier = "drainer-terminal-test"; + try { + const failedEvents: EventBusEventArgs<"runFailed">[0][] = []; + engine.eventBus.on("runFailed", (event) => { + failedEvents.push(event); + }); - const failed = await engine.createFailedTaskRun({ - friendlyId, - environment: { - id: authenticatedEnvironment.id, - type: authenticatedEnvironment.type, - project: { id: authenticatedEnvironment.project.id }, - organization: { id: authenticatedEnvironment.organization.id }, - }, - taskIdentifier, - payload: "{}", - payloadType: "application/json", - error: { - type: "STRING_ERROR", - raw: "Mollifier drainer terminal failure: synthetic engine.trigger panic", - }, - traceId: "0123456789abcdef0123456789abcdef", - spanId: "fedcba9876543210", - }); + const friendlyId = generateFriendlyId("run"); + const taskIdentifier = "drainer-terminal-test"; + + const failed = await engine.createFailedTaskRun({ + friendlyId, + environment: { + id: authenticatedEnvironment.id, + type: authenticatedEnvironment.type, + project: { id: authenticatedEnvironment.project.id }, + organization: { id: authenticatedEnvironment.organization.id }, + }, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + error: { + type: "STRING_ERROR", + raw: "Mollifier drainer terminal failure: synthetic engine.trigger panic", + }, + traceId: "0123456789abcdef0123456789abcdef", + spanId: "fedcba9876543210", + }); - expect(failed.status).toBe("SYSTEM_FAILURE"); + expect(failed.status).toBe("SYSTEM_FAILURE"); - expect(failedEvents).toHaveLength(1); - const event = failedEvents[0]; - expect(event.run.id).toBe(failed.id); - expect(event.run.status).toBe("SYSTEM_FAILURE"); - expect(event.run.spanId).toBe("fedcba9876543210"); - // attemptNumber=0 is the marker that the run never executed β€” - // it's a synthesised terminal failure, not an exhausted-retries - // failure. Downstream consumers can use this to distinguish. - expect(event.run.attemptNumber).toBe(0); - expect(event.run.usageDurationMs).toBe(0); - expect(event.run.costInCents).toBe(0); - expect(event.run.error).toEqual({ - type: "STRING_ERROR", - raw: "Mollifier drainer terminal failure: synthetic engine.trigger panic", - }); - expect(event.organization.id).toBe(authenticatedEnvironment.organization.id); - expect(event.project.id).toBe(authenticatedEnvironment.project.id); - expect(event.environment.id).toBe(authenticatedEnvironment.id); - } finally { - await engine.quit(); + expect(failedEvents).toHaveLength(1); + const event = failedEvents[0]; + expect(event.run.id).toBe(failed.id); + expect(event.run.status).toBe("SYSTEM_FAILURE"); + expect(event.run.spanId).toBe("fedcba9876543210"); + // attemptNumber=0 is the marker that the run never executed β€” + // it's a synthesised terminal failure, not an exhausted-retries + // failure. Downstream consumers can use this to distinguish. + expect(event.run.attemptNumber).toBe(0); + expect(event.run.usageDurationMs).toBe(0); + expect(event.run.costInCents).toBe(0); + expect(event.run.error).toEqual({ + type: "STRING_ERROR", + raw: "Mollifier drainer terminal failure: synthetic engine.trigger panic", + }); + expect(event.organization.id).toBe(authenticatedEnvironment.organization.id); + expect(event.project.id).toBe(authenticatedEnvironment.project.id); + expect(event.environment.id).toBe(authenticatedEnvironment.id); + } finally { + await engine.quit(); + } } - }); + ); // The TriggerFailedTaskService.call() path wraps createFailedTaskRun // inside `repository.traceEvent({ incomplete: false, isError: true })` @@ -126,7 +129,11 @@ describe("RunEngine.createFailedTaskRun", () => { const engine = new RunEngine({ prisma, worker: { redis: redisOptions, workers: 1, tasksPerWorker: 10, pollIntervalMs: 100 }, - queue: { redis: redisOptions, masterQueueConsumersDisabled: true, processWorkerQueueDebounceMs: 50 }, + queue: { + redis: redisOptions, + masterQueueConsumersDisabled: true, + processWorkerQueueDebounceMs: 50, + }, runLock: { redis: redisOptions }, machines: { defaultMachine: "small-1x", @@ -171,6 +178,6 @@ describe("RunEngine.createFailedTaskRun", () => { } finally { await engine.quit(); } - }, + } ); }); diff --git a/internal-packages/run-engine/src/engine/tests/debounce.test.ts b/internal-packages/run-engine/src/engine/tests/debounce.test.ts index e46f0de07c..089756c5bc 100644 --- a/internal-packages/run-engine/src/engine/tests/debounce.test.ts +++ b/internal-packages/run-engine/src/engine/tests/debounce.test.ts @@ -96,117 +96,114 @@ describe("RunEngine debounce", () => { } }); - containerTest( - "Debounce: multiple triggers return same run", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest("Debounce: multiple triggers return same run", 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, - }, - runLock: { - redis: redisOptions, - }, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, }, - baseCostInCents: 0.0001, - }, - debounce: { - maxDebounceDurationMs: 60_000, }, - tracer: trace.getTracer("test", "0.0.0"), - }); + baseCostInCents: 0.0001, + }, + debounce: { + maxDebounceDurationMs: 60_000, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - try { - const taskIdentifier = "test-task"; + try { + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // First trigger creates run - const run1 = await engine.trigger( - { - number: 1, - friendlyId: "run_deb1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: '{"data": "first"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: "task/test-task", - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 5000), - debounce: { - key: "user-123", - delay: "5s", - }, + // First trigger creates run + const run1 = await engine.trigger( + { + number: 1, + friendlyId: "run_deb1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: '{"data": "first"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: "task/test-task", + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "user-123", + delay: "5s", }, - prisma - ); + }, + prisma + ); - // Second trigger should return same run - const run2 = await engine.trigger( - { - number: 2, - friendlyId: "run_deb2", - environment: authenticatedEnvironment, - taskIdentifier, - payload: '{"data": "second"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12346", - spanId: "s12346", - workerQueue: "main", - queue: "task/test-task", - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 5000), - debounce: { - key: "user-123", - delay: "5s", - }, + // Second trigger should return same run + const run2 = await engine.trigger( + { + number: 2, + friendlyId: "run_deb2", + environment: authenticatedEnvironment, + taskIdentifier, + payload: '{"data": "second"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12346", + spanId: "s12346", + workerQueue: "main", + queue: "task/test-task", + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "user-123", + delay: "5s", }, - prisma - ); + }, + prisma + ); - // Both should return the same run (first run wins) - expect(run2.id).toBe(run1.id); - expect(run2.friendlyId).toBe(run1.friendlyId); + // Both should return the same run (first run wins) + expect(run2.id).toBe(run1.id); + expect(run2.friendlyId).toBe(run1.friendlyId); - // Only one run should exist in DB - const runs = await prisma.taskRun.findMany({ - where: { - taskIdentifier, - runtimeEnvironmentId: authenticatedEnvironment.id, - }, - }); - expect(runs.length).toBe(1); - } finally { - await engine.quit(); - } + // Only one run should exist in DB + const runs = await prisma.taskRun.findMany({ + where: { + taskIdentifier, + runtimeEnvironmentId: authenticatedEnvironment.id, + }, + }); + expect(runs.length).toBe(1); + } finally { + await engine.quit(); } - ); + }); containerTest( "Debounce: delay extension on subsequent triggers", @@ -441,91 +438,88 @@ describe("RunEngine debounce", () => { } ); - containerTest( - "Debounce: run executes after final delay", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest("Debounce: run executes after final delay", 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, - }, - runLock: { - redis: redisOptions, - }, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, }, - baseCostInCents: 0.0001, - }, - debounce: { - maxDebounceDurationMs: 60_000, }, - tracer: trace.getTracer("test", "0.0.0"), - }); + baseCostInCents: 0.0001, + }, + debounce: { + maxDebounceDurationMs: 60_000, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - try { - const taskIdentifier = "test-task"; + try { + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // First trigger with 1s delay - const run = await engine.trigger( - { - number: 1, - friendlyId: "run_deb1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: '{"data": "first"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: "task/test-task", - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 1000), - debounce: { - key: "user-123", - delay: "1s", - }, + // First trigger with 1s delay + const run = await engine.trigger( + { + number: 1, + friendlyId: "run_deb1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: '{"data": "first"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: "task/test-task", + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 1000), + debounce: { + key: "user-123", + delay: "1s", }, - prisma - ); + }, + prisma + ); - // Verify it's in DELAYED status - let executionData = await engine.getRunExecutionData({ runId: run.id }); - assertNonNullable(executionData); - expect(executionData.snapshot.executionStatus).toBe("DELAYED"); + // Verify it's in DELAYED status + let executionData = await engine.getRunExecutionData({ runId: run.id }); + assertNonNullable(executionData); + expect(executionData.snapshot.executionStatus).toBe("DELAYED"); - // Wait for delay to pass - await setTimeout(1500); + // Wait for delay to pass + await setTimeout(1500); - // Should now be QUEUED - executionData = await engine.getRunExecutionData({ runId: run.id }); - assertNonNullable(executionData); - expect(executionData.snapshot.executionStatus).toBe("QUEUED"); - } finally { - await engine.quit(); - } + // Should now be QUEUED + executionData = await engine.getRunExecutionData({ runId: run.id }); + assertNonNullable(executionData); + expect(executionData.snapshot.executionStatus).toBe("QUEUED"); + } finally { + await engine.quit(); } - ); + }); containerTest( "Debounce: no longer works after run is enqueued", @@ -620,122 +614,16 @@ describe("RunEngine debounce", () => { queue: "task/test-task", isTest: false, tags: [], - delayUntil: new Date(Date.now() + 5000), - debounce: { - key: "user-123", - delay: "5s", - }, - }, - prisma - ); - - // Should be a different run - expect(run2.id).not.toBe(run1.id); - } finally { - await engine.quit(); - } - } - ); - - containerTest( - "Debounce: max duration exceeded creates new run", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - - // Set a very short max debounce duration - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - redis: redisOptions, - }, - runLock: { - redis: redisOptions, - }, - machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, - }, - baseCostInCents: 0.0001, - }, - debounce: { - maxDebounceDurationMs: 500, // Very short max duration - }, - tracer: trace.getTracer("test", "0.0.0"), - }); - - try { - const taskIdentifier = "test-task"; - - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - - // First trigger - const run1 = await engine.trigger( - { - number: 1, - friendlyId: "run_deb1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: '{"data": "first"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: "task/test-task", - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 2000), - debounce: { - key: "user-123", - delay: "2s", - }, - }, - prisma - ); - - // Wait for max duration to be exceeded - await setTimeout(700); - - // Second trigger should create a new run because max duration exceeded - const run2 = await engine.trigger( - { - number: 2, - friendlyId: "run_deb2", - environment: authenticatedEnvironment, - taskIdentifier, - payload: '{"data": "second"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12346", - spanId: "s12346", - workerQueue: "main", - queue: "task/test-task", - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 2000), + delayUntil: new Date(Date.now() + 5000), debounce: { key: "user-123", - delay: "2s", + delay: "5s", }, }, prisma ); - // Should be a different run because max duration exceeded + // Should be a different run expect(run2.id).not.toBe(run1.id); } finally { await engine.quit(); @@ -744,10 +632,11 @@ describe("RunEngine debounce", () => { ); containerTest( - "Debounce keys are scoped to task identifier", + "Debounce: max duration exceeded creates new run", async ({ prisma, redisOptions }) => { const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + // Set a very short max debounce duration const engine = new RunEngine({ prisma, worker: { @@ -775,80 +664,182 @@ describe("RunEngine debounce", () => { baseCostInCents: 0.0001, }, debounce: { - maxDebounceDurationMs: 60_000, + maxDebounceDurationMs: 500, // Very short max duration }, tracer: trace.getTracer("test", "0.0.0"), }); try { - const taskIdentifier1 = "test-task-1"; - const taskIdentifier2 = "test-task-2"; + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier1); - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier2); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // Trigger task 1 with debounce key + // First trigger const run1 = await engine.trigger( { number: 1, - friendlyId: "run_task1", + friendlyId: "run_deb1", environment: authenticatedEnvironment, - taskIdentifier: taskIdentifier1, - payload: '{"data": "task1"}', + taskIdentifier, + payload: '{"data": "first"}', payloadType: "application/json", context: {}, traceContext: {}, traceId: "t12345", spanId: "s12345", workerQueue: "main", - queue: `task/${taskIdentifier1}`, + queue: "task/test-task", isTest: false, tags: [], - delayUntil: new Date(Date.now() + 5000), + delayUntil: new Date(Date.now() + 2000), debounce: { - key: "shared-key", - delay: "5s", + key: "user-123", + delay: "2s", }, }, prisma ); - // Trigger task 2 with same debounce key - should create separate run + // Wait for max duration to be exceeded + await setTimeout(700); + + // Second trigger should create a new run because max duration exceeded const run2 = await engine.trigger( { number: 2, - friendlyId: "run_task2", + friendlyId: "run_deb2", environment: authenticatedEnvironment, - taskIdentifier: taskIdentifier2, - payload: '{"data": "task2"}', + taskIdentifier, + payload: '{"data": "second"}', payloadType: "application/json", context: {}, traceContext: {}, traceId: "t12346", spanId: "s12346", workerQueue: "main", - queue: `task/${taskIdentifier2}`, + queue: "task/test-task", isTest: false, tags: [], - delayUntil: new Date(Date.now() + 5000), + delayUntil: new Date(Date.now() + 2000), debounce: { - key: "shared-key", - delay: "5s", + key: "user-123", + delay: "2s", }, }, prisma ); - // Should be different runs (debounce scoped to task) + // Should be a different run because max duration exceeded expect(run2.id).not.toBe(run1.id); - expect(run1.taskIdentifier).toBe(taskIdentifier1); - expect(run2.taskIdentifier).toBe(taskIdentifier2); } finally { await engine.quit(); } } ); + containerTest("Debounce keys are scoped to task identifier", 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, + }, + 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, + }, + debounce: { + maxDebounceDurationMs: 60_000, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); + + try { + const taskIdentifier1 = "test-task-1"; + const taskIdentifier2 = "test-task-2"; + + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier1); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier2); + + // Trigger task 1 with debounce key + const run1 = await engine.trigger( + { + number: 1, + friendlyId: "run_task1", + environment: authenticatedEnvironment, + taskIdentifier: taskIdentifier1, + payload: '{"data": "task1"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: `task/${taskIdentifier1}`, + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "shared-key", + delay: "5s", + }, + }, + prisma + ); + + // Trigger task 2 with same debounce key - should create separate run + const run2 = await engine.trigger( + { + number: 2, + friendlyId: "run_task2", + environment: authenticatedEnvironment, + taskIdentifier: taskIdentifier2, + payload: '{"data": "task2"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12346", + spanId: "s12346", + workerQueue: "main", + queue: `task/${taskIdentifier2}`, + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "shared-key", + delay: "5s", + }, + }, + prisma + ); + + // Should be different runs (debounce scoped to task) + expect(run2.id).not.toBe(run1.id); + expect(run1.taskIdentifier).toBe(taskIdentifier1); + expect(run2.taskIdentifier).toBe(taskIdentifier2); + } finally { + await engine.quit(); + } + }); + containerTest( "Debounce with triggerAndWait: parent blocked by debounced child run", async ({ prisma, redisOptions }) => { @@ -1219,130 +1210,127 @@ describe("RunEngine debounce", () => { } ); - containerTest( - "Debounce: keys scoped to environment", - async ({ prisma, redisOptions }) => { - // Create production environment (also creates org and project) - const prodEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - - // Create a second environment (development) within the same org/project - const devEnvironment = await prisma.runtimeEnvironment.create({ - data: { - type: "DEVELOPMENT", - slug: "dev-slug", - projectId: prodEnvironment.projectId, - organizationId: prodEnvironment.organizationId, - apiKey: "dev_api_key", - pkApiKey: "dev_pk_api_key", - shortcode: "dev_short", - maximumConcurrencyLimit: 10, - }, - include: { - project: true, - organization: true, - orgMember: true, - }, - }); + containerTest("Debounce: keys scoped to environment", async ({ prisma, redisOptions }) => { + // Create production environment (also creates org and project) + const prodEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + + // Create a second environment (development) within the same org/project + const devEnvironment = await prisma.runtimeEnvironment.create({ + data: { + type: "DEVELOPMENT", + slug: "dev-slug", + projectId: prodEnvironment.projectId, + organizationId: prodEnvironment.organizationId, + apiKey: "dev_api_key", + pkApiKey: "dev_pk_api_key", + shortcode: "dev_short", + maximumConcurrencyLimit: 10, + }, + include: { + project: true, + organization: true, + orgMember: true, + }, + }); - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - redis: redisOptions, - }, - runLock: { - redis: redisOptions, - }, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, }, - baseCostInCents: 0.0001, - }, - debounce: { - maxDebounceDurationMs: 60_000, }, - tracer: trace.getTracer("test", "0.0.0"), - }); + baseCostInCents: 0.0001, + }, + debounce: { + maxDebounceDurationMs: 60_000, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - try { - const taskIdentifier = "test-task"; + try { + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, prodEnvironment, taskIdentifier); - await setupBackgroundWorker(engine, devEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, prodEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, devEnvironment, taskIdentifier); - // Trigger in production environment - const runProd = await engine.trigger( - { - number: 1, - friendlyId: "run_prod1", - environment: prodEnvironment, - taskIdentifier, - payload: '{"env": "prod"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: `task/${taskIdentifier}`, - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 5000), - debounce: { - key: "same-key", - delay: "5s", - }, + // Trigger in production environment + const runProd = await engine.trigger( + { + number: 1, + friendlyId: "run_prod1", + environment: prodEnvironment, + taskIdentifier, + payload: '{"env": "prod"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: `task/${taskIdentifier}`, + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "same-key", + delay: "5s", }, - prisma - ); + }, + prisma + ); - // Trigger in development environment with same key - should create separate run - const runDev = await engine.trigger( - { - number: 2, - friendlyId: "run_dev1", - environment: devEnvironment, - taskIdentifier, - payload: '{"env": "dev"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12346", - spanId: "s12346", - workerQueue: "main", - queue: `task/${taskIdentifier}`, - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 5000), - debounce: { - key: "same-key", - delay: "5s", - }, + // Trigger in development environment with same key - should create separate run + const runDev = await engine.trigger( + { + number: 2, + friendlyId: "run_dev1", + environment: devEnvironment, + taskIdentifier, + payload: '{"env": "dev"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12346", + spanId: "s12346", + workerQueue: "main", + queue: `task/${taskIdentifier}`, + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "same-key", + delay: "5s", }, - prisma - ); + }, + prisma + ); - // Should be different runs (debounce scoped to environment) - expect(runDev.id).not.toBe(runProd.id); - expect(runProd.runtimeEnvironmentId).toBe(prodEnvironment.id); - expect(runDev.runtimeEnvironmentId).toBe(devEnvironment.id); - } finally { - await engine.quit(); - } + // Should be different runs (debounce scoped to environment) + expect(runDev.id).not.toBe(runProd.id); + expect(runProd.runtimeEnvironmentId).toBe(prodEnvironment.id); + expect(runDev.runtimeEnvironmentId).toBe(devEnvironment.id); + } finally { + await engine.quit(); } - ); + }); containerTest( "Debounce: concurrent triggers only create one run (distributed race protection)", @@ -2957,13 +2945,7 @@ describe("RunEngine debounce", () => { assertNonNullable(originalDelayUntil); try { - const blockResult = await blockingRedis.set( - run1.id, - "test-blocker", - "PX", - 30_000, - "NX" - ); + const blockResult = await blockingRedis.set(run1.id, "test-blocker", "PX", 30_000, "NX"); expect(blockResult).toBe("OK"); const run2 = await engine.trigger( @@ -3196,4 +3178,3 @@ describe("RunEngine debounce", () => { ); } }); - diff --git a/internal-packages/run-engine/src/engine/tests/dequeuing.test.ts b/internal-packages/run-engine/src/engine/tests/dequeuing.test.ts index 5871bee832..fd67ff2cf3 100644 --- a/internal-packages/run-engine/src/engine/tests/dequeuing.test.ts +++ b/internal-packages/run-engine/src/engine/tests/dequeuing.test.ts @@ -160,9 +160,8 @@ describe("RunEngine dequeuing", () => { expect(executionDataBefore.snapshot.executionStatus).toBe("PENDING_EXECUTING"); // Verify run is in concurrency - const envConcurrencyBefore = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyBefore = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyBefore).toBe(1); // Simulate DB failure fallback: call nackMessage directly via Redis @@ -174,9 +173,8 @@ describe("RunEngine dequeuing", () => { // Verify concurrency is cleared - this is the key fix! // Without this fix, the run would stay in concurrency sets forever - const envConcurrencyAfter = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyAfter = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyAfter).toBe(0); // Verify the message is back in the queue diff --git a/internal-packages/run-engine/src/engine/tests/getSnapshotsSince.test.ts b/internal-packages/run-engine/src/engine/tests/getSnapshotsSince.test.ts index 15831d1083..f48c7c37c9 100644 --- a/internal-packages/run-engine/src/engine/tests/getSnapshotsSince.test.ts +++ b/internal-packages/run-engine/src/engine/tests/getSnapshotsSince.test.ts @@ -13,10 +13,7 @@ import { setupTestScenario, generateLargeOutput, } from "./helpers/snapshotTestHelpers.js"; -import { - copySnapshotsToReplica, - createTestMetricsMeter, -} from "./helpers/replicaTestHelpers.js"; +import { copySnapshotsToReplica, createTestMetricsMeter } from "./helpers/replicaTestHelpers.js"; import { generateFriendlyId } from "@trigger.dev/core/v3/isomorphic"; vi.setConfig({ testTimeout: 120_000 }); 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 f981f35145..a4084a8c19 100644 --- a/internal-packages/run-engine/src/engine/tests/helpers/snapshotTestHelpers.ts +++ b/internal-packages/run-engine/src/engine/tests/helpers/snapshotTestHelpers.ts @@ -294,7 +294,9 @@ export async function setupTestScenario( } // Get the waitpoint IDs that should be "completed" at this snapshot - const completedWaitpointIds = waitpoints.slice(0, config.completedWaitpointCount).map((w) => w.id); + const completedWaitpointIds = waitpoints + .slice(0, config.completedWaitpointCount) + .map((w) => w.id); const snapshot = await createTestSnapshot(prisma, { runId: run.id, diff --git a/internal-packages/run-engine/src/engine/tests/lazyWaitpoint.test.ts b/internal-packages/run-engine/src/engine/tests/lazyWaitpoint.test.ts index dedcff5b5b..d45e3bcd6c 100644 --- a/internal-packages/run-engine/src/engine/tests/lazyWaitpoint.test.ts +++ b/internal-packages/run-engine/src/engine/tests/lazyWaitpoint.test.ts @@ -196,298 +196,289 @@ describe("RunEngine lazy waitpoint creation", () => { } }); - containerTest( - "Completion without waitpoint succeeds", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest("Completion without waitpoint succeeds", 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, - masterQueueConsumersDisabled: true, - processWorkerQueueDebounceMs: 50, - }, - runLock: { - redis: redisOptions, - }, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + masterQueueConsumersDisabled: true, + processWorkerQueueDebounceMs: 50, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "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"), - }); + baseCostInCents: 0.0001, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - try { - const taskIdentifier = "test-task"; + try { + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // Trigger a standalone run (no waitpoint) - const run = await engine.trigger( - { - number: 1, - friendlyId: "run_complete1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: "{}", - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: `task/${taskIdentifier}`, - isTest: false, - tags: [], - }, - prisma - ); + // Trigger a standalone run (no waitpoint) + const run = await engine.trigger( + { + number: 1, + friendlyId: "run_complete1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: `task/${taskIdentifier}`, + isTest: false, + tags: [], + }, + prisma + ); - // Verify no waitpoint - const dbRun = await prisma.taskRun.findFirst({ - where: { id: run.id }, - include: { associatedWaitpoint: true }, - }); - assertNonNullable(dbRun); - expect(dbRun.associatedWaitpoint).toBeNull(); + // Verify no waitpoint + const dbRun = await prisma.taskRun.findFirst({ + where: { id: run.id }, + include: { associatedWaitpoint: true }, + }); + assertNonNullable(dbRun); + expect(dbRun.associatedWaitpoint).toBeNull(); - // Dequeue and start the run - await setTimeout(500); - const dequeued = await engine.dequeueFromWorkerQueue({ - consumerId: "test_12345", - workerQueue: "main", - }); - const attemptResult = await engine.startRunAttempt({ - runId: run.id, - snapshotId: dequeued[0].snapshot.id, - }); + // Dequeue and start the run + await setTimeout(500); + const dequeued = await engine.dequeueFromWorkerQueue({ + consumerId: "test_12345", + workerQueue: "main", + }); + const attemptResult = await engine.startRunAttempt({ + runId: run.id, + snapshotId: dequeued[0].snapshot.id, + }); - // Complete the run - should NOT throw even without waitpoint - const completeResult = await engine.completeRunAttempt({ - runId: run.id, - snapshotId: attemptResult.snapshot.id, - completion: { - id: run.id, - ok: true, - output: '{"result":"success"}', - outputType: "application/json", - }, - }); + // Complete the run - should NOT throw even without waitpoint + const completeResult = await engine.completeRunAttempt({ + runId: run.id, + snapshotId: attemptResult.snapshot.id, + completion: { + id: run.id, + ok: true, + output: '{"result":"success"}', + outputType: "application/json", + }, + }); - // Verify run completed successfully - expect(completeResult.attemptStatus).toBe("RUN_FINISHED"); - const executionData = await engine.getRunExecutionData({ runId: run.id }); - assertNonNullable(executionData); - expect(executionData.run.status).toBe("COMPLETED_SUCCESSFULLY"); - expect(executionData.snapshot.executionStatus).toBe("FINISHED"); - } finally { - await engine.quit(); - } + // Verify run completed successfully + expect(completeResult.attemptStatus).toBe("RUN_FINISHED"); + const executionData = await engine.getRunExecutionData({ runId: run.id }); + assertNonNullable(executionData); + expect(executionData.run.status).toBe("COMPLETED_SUCCESSFULLY"); + expect(executionData.snapshot.executionStatus).toBe("FINISHED"); + } finally { + await engine.quit(); } - ); + }); - containerTest( - "Cancellation without waitpoint succeeds", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest("Cancellation without waitpoint succeeds", 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, - masterQueueConsumersDisabled: true, - processWorkerQueueDebounceMs: 50, - }, - runLock: { - redis: redisOptions, - }, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + masterQueueConsumersDisabled: true, + processWorkerQueueDebounceMs: 50, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "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"), - }); + baseCostInCents: 0.0001, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - try { - const taskIdentifier = "test-task"; + try { + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // Trigger a standalone run (no waitpoint) - const run = await engine.trigger( - { - number: 1, - friendlyId: "run_cancel1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: "{}", - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: `task/${taskIdentifier}`, - isTest: false, - tags: [], - }, - prisma - ); + // Trigger a standalone run (no waitpoint) + const run = await engine.trigger( + { + number: 1, + friendlyId: "run_cancel1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: `task/${taskIdentifier}`, + isTest: false, + tags: [], + }, + prisma + ); - // Verify no waitpoint - const dbRun = await prisma.taskRun.findFirst({ - where: { id: run.id }, - include: { associatedWaitpoint: true }, - }); - assertNonNullable(dbRun); - expect(dbRun.associatedWaitpoint).toBeNull(); + // Verify no waitpoint + const dbRun = await prisma.taskRun.findFirst({ + where: { id: run.id }, + include: { associatedWaitpoint: true }, + }); + assertNonNullable(dbRun); + expect(dbRun.associatedWaitpoint).toBeNull(); - // Cancel the run - should NOT throw even without waitpoint - const cancelResult = await engine.cancelRun({ - runId: run.id, - reason: "Test cancellation", - }); + // Cancel the run - should NOT throw even without waitpoint + const cancelResult = await engine.cancelRun({ + runId: run.id, + reason: "Test cancellation", + }); - // Verify run was cancelled - expect(cancelResult.alreadyFinished).toBe(false); - const executionData = await engine.getRunExecutionData({ runId: run.id }); - assertNonNullable(executionData); - expect(executionData.run.status).toBe("CANCELED"); - expect(executionData.snapshot.executionStatus).toBe("FINISHED"); - } finally { - await engine.quit(); - } + // Verify run was cancelled + expect(cancelResult.alreadyFinished).toBe(false); + const executionData = await engine.getRunExecutionData({ runId: run.id }); + assertNonNullable(executionData); + expect(executionData.run.status).toBe("CANCELED"); + expect(executionData.snapshot.executionStatus).toBe("FINISHED"); + } finally { + await engine.quit(); } - ); + }); - containerTest( - "TTL expiration without waitpoint succeeds", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest("TTL expiration without waitpoint succeeds", async ({ prisma, redisOptions }) => { + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + masterQueueConsumersDisabled: true, + processWorkerQueueDebounceMs: 50, + ttlSystem: { pollIntervalMs: 100, + batchSize: 10, + batchMaxWaitMs: 100, }, - queue: { - redis: redisOptions, - masterQueueConsumersDisabled: true, - processWorkerQueueDebounceMs: 50, - ttlSystem: { - pollIntervalMs: 100, - batchSize: 10, - batchMaxWaitMs: 100, - }, - }, - runLock: { - redis: redisOptions, - }, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "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"; + baseCostInCents: 0.0001, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + try { + const taskIdentifier = "test-task"; - // TTL only expires runs still queued waiting on a concurrency slot. - await engine.runQueue.updateEnvConcurrencyLimits({ - ...authenticatedEnvironment, - maximumConcurrencyLimit: 0, - }); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // Trigger a standalone run with TTL (no waitpoint) - const run = await engine.trigger( - { - number: 1, - friendlyId: "run_ttl1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: "{}", - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: `task/${taskIdentifier}`, - isTest: false, - tags: [], - ttl: "1s", - }, - prisma - ); - - // Verify no waitpoint - const dbRun = await prisma.taskRun.findFirst({ - where: { id: run.id }, - include: { associatedWaitpoint: true }, - }); - assertNonNullable(dbRun); - expect(dbRun.associatedWaitpoint).toBeNull(); + // TTL only expires runs still queued waiting on a concurrency slot. + await engine.runQueue.updateEnvConcurrencyLimits({ + ...authenticatedEnvironment, + maximumConcurrencyLimit: 0, + }); - // Wait for TTL to expire - await setTimeout(1_500); + // Trigger a standalone run with TTL (no waitpoint) + const run = await engine.trigger( + { + number: 1, + friendlyId: "run_ttl1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: `task/${taskIdentifier}`, + isTest: false, + tags: [], + ttl: "1s", + }, + prisma + ); - // Verify run expired successfully (no throw). - // The batch TTL path does not create execution snapshots, so check - // the status directly from the database rather than via - // getRunExecutionData. - const expiredRun = await prisma.taskRun.findUnique({ - where: { id: run.id }, - select: { status: true }, - }); - expect(expiredRun?.status).toBe("EXPIRED"); - } finally { - await engine.quit(); - } + // Verify no waitpoint + const dbRun = await prisma.taskRun.findFirst({ + where: { id: run.id }, + include: { associatedWaitpoint: true }, + }); + assertNonNullable(dbRun); + expect(dbRun.associatedWaitpoint).toBeNull(); + + // Wait for TTL to expire + await setTimeout(1_500); + + // Verify run expired successfully (no throw). + // The batch TTL path does not create execution snapshots, so check + // the status directly from the database rather than via + // getRunExecutionData. + const expiredRun = await prisma.taskRun.findUnique({ + where: { id: run.id }, + select: { status: true }, + }); + expect(expiredRun?.status).toBe("EXPIRED"); + } finally { + await engine.quit(); } - ); + }); containerTest( "getOrCreateRunWaitpoint: returns existing waitpoint", diff --git a/internal-packages/run-engine/src/engine/tests/trigger.test.ts b/internal-packages/run-engine/src/engine/tests/trigger.test.ts index a4f500e72a..3bd0c833d1 100644 --- a/internal-packages/run-engine/src/engine/tests/trigger.test.ts +++ b/internal-packages/run-engine/src/engine/tests/trigger.test.ts @@ -103,9 +103,8 @@ describe("RunEngine trigger()", () => { expect(queueLength).toBe(1); //concurrency before - const envConcurrencyBefore = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyBefore = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyBefore).toBe(0); await setTimeout(500); @@ -119,9 +118,8 @@ describe("RunEngine trigger()", () => { expect(dequeued[0].run.id).toBe(run.id); expect(dequeued[0].run.attemptNumber).toBe(1); - const envConcurrencyAfter = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyAfter = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyAfter).toBe(1); let attemptEvent: EventBusEventArgs<"runAttemptStarted">[0] | undefined = undefined; @@ -186,9 +184,8 @@ describe("RunEngine trigger()", () => { expect(completedEvent.run.outputType).toBe("application/json"); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); //standalone triggers don't create waitpoints, so none should exist @@ -312,9 +309,8 @@ describe("RunEngine trigger()", () => { expect(executionData3.run.status).toBe("COMPLETED_WITH_ERRORS"); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); //standalone triggers don't create waitpoints, so none should exist @@ -526,5 +522,4 @@ describe("RunEngine trigger()", () => { } } ); - }); diff --git a/internal-packages/run-engine/src/engine/tests/ttl.test.ts b/internal-packages/run-engine/src/engine/tests/ttl.test.ts index 13d4c55b66..52b2645207 100644 --- a/internal-packages/run-engine/src/engine/tests/ttl.test.ts +++ b/internal-packages/run-engine/src/engine/tests/ttl.test.ts @@ -115,9 +115,8 @@ describe("RunEngine ttl", () => { expect(expiredRun?.status).toBe("EXPIRED"); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); // Queue sorted set should be empty (run removed from queue) @@ -634,9 +633,8 @@ describe("RunEngine ttl", () => { } // Concurrency should be released for all - const envConcurrency = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrency = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrency).toBe(0); // Queue sorted set should be empty (all runs removed from queue) @@ -986,9 +984,8 @@ describe("RunEngine ttl", () => { expect(expiredRunData?.status).toBe("EXPIRED"); // Concurrency should be released - const envConcurrency = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrency = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrency).toBe(0); } finally { await engine.quit(); @@ -1070,9 +1067,8 @@ describe("RunEngine ttl", () => { await engine.runQueue.redis.sadd(envConcurrencyKey, run.id); await engine.runQueue.redis.sadd(envDequeuedKey, run.id); - const concurrencyBefore = await engine.runQueue.getCurrentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const concurrencyBefore = + await engine.runQueue.getCurrentConcurrencyOfEnvironment(authenticatedEnvironment); expect(concurrencyBefore).toContain(run.id); await setTimeout(1_500); @@ -1090,9 +1086,8 @@ describe("RunEngine ttl", () => { { timeout: 15_000, interval: 200 } ); - const concurrencyAfter = await engine.runQueue.getCurrentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const concurrencyAfter = + await engine.runQueue.getCurrentConcurrencyOfEnvironment(authenticatedEnvironment); expect(concurrencyAfter).not.toContain(run.id); const stillInDequeued = await engine.runQueue.redis.sismember(envDequeuedKey, run.id); diff --git a/internal-packages/run-engine/src/run-queue/index.ts b/internal-packages/run-engine/src/run-queue/index.ts index 9808f96f41..c100335757 100644 --- a/internal-packages/run-engine/src/run-queue/index.ts +++ b/internal-packages/run-engine/src/run-queue/index.ts @@ -387,7 +387,7 @@ export class RunQueue { const burstFactor = result ? Number(result) - : this.options.defaultEnvConcurrencyBurstFactor ?? 1; + : (this.options.defaultEnvConcurrencyBurstFactor ?? 1); const limit = await this.getEnvConcurrencyLimit(env); @@ -399,7 +399,7 @@ export class RunQueue { const burstFactor = result ? Number(result) - : this.options.defaultEnvConcurrencyBurstFactor ?? 1; + : (this.options.defaultEnvConcurrencyBurstFactor ?? 1); return burstFactor; } @@ -1389,9 +1389,7 @@ export class RunQueue { })) { const now = Date.now(); - const [error, expiredRuns] = await tryCatch( - this.#expireTtlRuns(shard, now, batchSize) - ); + const [error, expiredRuns] = await tryCatch(this.#expireTtlRuns(shard, now, batchSize)); if (error) { this.logger.error(`Failed to expire TTL runs for shard ${shard}`, { @@ -1858,8 +1856,9 @@ export class RunQueue { const workerQueueKey = this.keys.workerQueueKey(message.workerQueue); const queueConcurrencyLimitKey = this.keys.queueConcurrencyLimitKeyFromQueue(message.queue); const envConcurrencyLimitKey = this.keys.envConcurrencyLimitKeyFromQueue(message.queue); - const envConcurrencyLimitBurstFactorKey = - this.keys.envConcurrencyLimitBurstFactorKeyFromQueue(message.queue); + const envConcurrencyLimitBurstFactorKey = this.keys.envConcurrencyLimitBurstFactorKeyFromQueue( + message.queue + ); // The value stored in the worker queue list β€” used to look up the message payload on dequeue const messageKeyValue = messageKey; @@ -2064,9 +2063,7 @@ export class RunQueue { this.keys.envIdFromQueue(messageQueue), ttlShardCount ); - const ttlQueueKey = this.options.ttlSystem - ? this.keys.ttlQueueKeyForShard(ttlShard) - : ""; + const ttlQueueKey = this.options.ttlSystem ? this.keys.ttlQueueKeyForShard(ttlShard) : ""; this.logger.debug("#callDequeueMessagesFromQueue", { messageQueue, @@ -2172,13 +2169,11 @@ export class RunQueue { }); const ckIndexKey = this.keys.ckIndexKeyFromQueue(ckWildcardQueue); - const queueConcurrencyLimitKey = - this.keys.queueConcurrencyLimitKeyFromQueue(ckWildcardQueue); + const queueConcurrencyLimitKey = this.keys.queueConcurrencyLimitKeyFromQueue(ckWildcardQueue); const envConcurrencyLimitKey = this.keys.envConcurrencyLimitKeyFromQueue(ckWildcardQueue); const envConcurrencyLimitBurstFactorKey = this.keys.envConcurrencyLimitBurstFactorKeyFromQueue(ckWildcardQueue); - const envCurrentConcurrencyKey = - this.keys.envCurrentConcurrencyKeyFromQueue(ckWildcardQueue); + const envCurrentConcurrencyKey = this.keys.envCurrentConcurrencyKeyFromQueue(ckWildcardQueue); const messageKeyPrefix = this.keys.messageKeyPrefixFromQueue(ckWildcardQueue); const envQueueKey = this.keys.envQueueKeyFromQueue(ckWildcardQueue); const masterQueueKey = this.keys.masterQueueKeyForShard(shard); @@ -2189,9 +2184,7 @@ export class RunQueue { this.keys.envIdFromQueue(ckWildcardQueue), ttlShardCount ); - const ttlQueueKey = this.options.ttlSystem - ? this.keys.ttlQueueKeyForShard(ttlShard) - : ""; + const ttlQueueKey = this.options.ttlSystem ? this.keys.ttlQueueKeyForShard(ttlShard) : ""; this.logger.debug("#callDequeueMessagesFromCkQueue", { ckWildcardQueue, diff --git a/internal-packages/run-engine/src/run-queue/tests/ckCounters.test.ts b/internal-packages/run-engine/src/run-queue/tests/ckCounters.test.ts index 23213bfe15..c08b3a515e 100644 --- a/internal-packages/run-engine/src/run-queue/tests/ckCounters.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/ckCounters.test.ts @@ -74,184 +74,168 @@ function makeMessage(overrides: Partial = {}): InputPayload { vi.setConfig({ testTimeout: 60_000 }); describe("CK base-queue counters", () => { - redisTest( - "lengthOfQueue returns aggregate across CK variants", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now(); - const messages = [ - makeMessage({ runId: "r1", concurrencyKey: "ck-a", timestamp: now }), - makeMessage({ runId: "r2", concurrencyKey: "ck-a", timestamp: now + 1 }), - makeMessage({ runId: "r3", concurrencyKey: "ck-b", timestamp: now + 2 }), - makeMessage({ runId: "r4", concurrencyKey: "ck-c", timestamp: now + 3 }), - ]; - - for (const msg of messages) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } + redisTest("lengthOfQueue returns aggregate across CK variants", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now(); + const messages = [ + makeMessage({ runId: "r1", concurrencyKey: "ck-a", timestamp: now }), + makeMessage({ runId: "r2", concurrencyKey: "ck-a", timestamp: now + 1 }), + makeMessage({ runId: "r3", concurrencyKey: "ck-b", timestamp: now + 2 }), + makeMessage({ runId: "r4", concurrencyKey: "ck-c", timestamp: now + 3 }), + ]; + + for (const msg of messages) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + } - // Aggregate (no CK arg) should sum all variants - expect(await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue)).toBe(4); + // Aggregate (no CK arg) should sum all variants + expect(await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue)).toBe(4); - // Per-variant still works - expect( - await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue, "ck-a") - ).toBe(2); - expect( - await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue, "ck-b") - ).toBe(1); + // Per-variant still works + expect(await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue, "ck-a")).toBe(2); + expect(await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue, "ck-b")).toBe(1); - // Plural lengthOfQueues should also see the aggregate - const lengths = await queue.lengthOfQueues(authenticatedEnvDev, [messages[0].queue]); - expect(lengths[messages[0].queue]).toBe(4); - } finally { - await queue.quit(); - } + // Plural lengthOfQueues should also see the aggregate + const lengths = await queue.lengthOfQueues(authenticatedEnvDev, [messages[0].queue]); + expect(lengths[messages[0].queue]).toBe(4); + } finally { + await queue.quit(); } - ); + }); - redisTest( - "lazy init from pre-existing CK backlog", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now(); - const baseMsg = makeMessage({ runId: "seed", concurrencyKey: "ck-a", timestamp: now }); - - // Pre-populate two variants via direct ZADD to simulate pre-deploy backlog - // (no counter touched). ioredis auto-prefixes keys with `runqueue:test:`, - // so we pass un-prefixed keys. - const variantA = testOptions.keys.queueKey(authenticatedEnvDev, baseMsg.queue, "ck-a"); - const variantB = testOptions.keys.queueKey(authenticatedEnvDev, baseMsg.queue, "ck-b"); - const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue(variantA); - for (let i = 0; i < 10; i++) { - await queue.redis.zadd(variantA, now + i, `old-a-${i}`); - } - for (let i = 0; i < 5; i++) { - await queue.redis.zadd(variantB, now + i, `old-b-${i}`); - } - await queue.redis.zadd(ckIndexKey, now, variantA); - await queue.redis.zadd(ckIndexKey, now, variantB); + redisTest("lazy init from pre-existing CK backlog", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now(); + const baseMsg = makeMessage({ runId: "seed", concurrencyKey: "ck-a", timestamp: now }); + + // Pre-populate two variants via direct ZADD to simulate pre-deploy backlog + // (no counter touched). ioredis auto-prefixes keys with `runqueue:test:`, + // so we pass un-prefixed keys. + const variantA = testOptions.keys.queueKey(authenticatedEnvDev, baseMsg.queue, "ck-a"); + const variantB = testOptions.keys.queueKey(authenticatedEnvDev, baseMsg.queue, "ck-b"); + const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue(variantA); + for (let i = 0; i < 10; i++) { + await queue.redis.zadd(variantA, now + i, `old-a-${i}`); + } + for (let i = 0; i < 5; i++) { + await queue.redis.zadd(variantB, now + i, `old-b-${i}`); + } + await queue.redis.zadd(ckIndexKey, now, variantA); + await queue.redis.zadd(ckIndexKey, now, variantB); + + // Counter should not yet exist + const counterKey = testOptions.keys.queueLengthCounterKeyFromQueue(variantA); + expect(await queue.redis.exists(counterKey)).toBe(0); + + // First CK enqueue: lazy init should compute 15 (pre-state), then INCR to 16 + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: "new-a", concurrencyKey: "ck-a", timestamp: now + 100 }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); - // Counter should not yet exist - const counterKey = testOptions.keys.queueLengthCounterKeyFromQueue(variantA); - expect(await queue.redis.exists(counterKey)).toBe(0); + const counterVal = await queue.redis.get(counterKey); + expect(Number(counterVal)).toBe(16); - // First CK enqueue: lazy init should compute 15 (pre-state), then INCR to 16 + // lengthOfQueue should also reflect 16 + expect(await queue.lengthOfQueue(authenticatedEnvDev, baseMsg.queue)).toBe(16); + } finally { + await queue.quit(); + } + }); + + redisTest("non-CK queue regression: counter never created", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + for (let i = 0; i < 5; i++) { await queue.enqueueMessage({ env: authenticatedEnvDev, - message: makeMessage({ runId: "new-a", concurrencyKey: "ck-a", timestamp: now + 100 }), + message: makeMessage({ runId: `r${i}`, timestamp: Date.now() + i }), workerQueue: authenticatedEnvDev.id, skipDequeueProcessing: true, }); - - const counterVal = await queue.redis.get(counterKey); - expect(Number(counterVal)).toBe(16); - - // lengthOfQueue should also reflect 16 - expect(await queue.lengthOfQueue(authenticatedEnvDev, baseMsg.queue)).toBe(16); - } finally { - await queue.quit(); } - } - ); - redisTest( - "non-CK queue regression: counter never created", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - for (let i = 0; i < 5; i++) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ runId: `r${i}`, timestamp: Date.now() + i }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - - // Counter key should not exist for a pure non-CK queue - const counterKey = testOptions.keys.queueLengthCounterKey(authenticatedEnvDev, "task/my-task"); - expect(await queue.redis.exists(counterKey)).toBe(0); - - // But lengthOfQueue still returns 5 via base ZCARD - expect(await queue.lengthOfQueue(authenticatedEnvDev, "task/my-task")).toBe(5); - } finally { - await queue.quit(); - } + // Counter key should not exist for a pure non-CK queue + const counterKey = testOptions.keys.queueLengthCounterKey( + authenticatedEnvDev, + "task/my-task" + ); + expect(await queue.redis.exists(counterKey)).toBe(0); + + // But lengthOfQueue still returns 5 via base ZCARD + expect(await queue.lengthOfQueue(authenticatedEnvDev, "task/my-task")).toBe(5); + } finally { + await queue.quit(); } - ); - - redisTest( - "mixed CK + non-CK on same base queue", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - // 3 non-CK + 2 CK on same base queue name - for (let i = 0; i < 3; i++) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ runId: `nonck-${i}`, timestamp: Date.now() + i }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - for (let i = 0; i < 2; i++) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ - runId: `ck-${i}`, - concurrencyKey: "ck-a", - timestamp: Date.now() + 100 + i, - }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } + }); - expect(await queue.lengthOfQueue(authenticatedEnvDev, "task/my-task")).toBe(5); - } finally { - await queue.quit(); + redisTest("mixed CK + non-CK on same base queue", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + // 3 non-CK + 2 CK on same base queue name + for (let i = 0; i < 3; i++) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: `nonck-${i}`, timestamp: Date.now() + i }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); } + for (let i = 0; i < 2; i++) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ + runId: `ck-${i}`, + concurrencyKey: "ck-a", + timestamp: Date.now() + 100 + i, + }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + } + + expect(await queue.lengthOfQueue(authenticatedEnvDev, "task/my-task")).toBe(5); + } finally { + await queue.quit(); } - ); + }); - redisTest( - "length counter decrements on dequeue", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; - const msgs = [ - makeMessage({ runId: "r1", concurrencyKey: "ck-a", timestamp: now }), - makeMessage({ runId: "r2", concurrencyKey: "ck-b", timestamp: now + 1 }), - ]; - for (const msg of msgs) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - expect(await queue.lengthOfQueue(authenticatedEnvDev, msgs[0].queue)).toBe(2); + redisTest("length counter decrements on dequeue", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; + const msgs = [ + makeMessage({ runId: "r1", concurrencyKey: "ck-a", timestamp: now }), + makeMessage({ runId: "r2", concurrencyKey: "ck-b", timestamp: now + 1 }), + ]; + for (const msg of msgs) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + } + expect(await queue.lengthOfQueue(authenticatedEnvDev, msgs[0].queue)).toBe(2); - const shard = testOptions.keys.masterQueueShardForEnvironment(msgs[0].environmentId, 2); - await queue.testDequeueFromMasterQueue(shard, msgs[0].environmentId, 10); + const shard = testOptions.keys.masterQueueShardForEnvironment(msgs[0].environmentId, 2); + await queue.testDequeueFromMasterQueue(shard, msgs[0].environmentId, 10); - // Both dequeued β†’ counter should be 0 - expect(await queue.lengthOfQueue(authenticatedEnvDev, msgs[0].queue)).toBe(0); - } finally { - await queue.quit(); - } + // Both dequeued β†’ counter should be 0 + expect(await queue.lengthOfQueue(authenticatedEnvDev, msgs[0].queue)).toBe(0); + } finally { + await queue.quit(); } - ); + }); redisTest( "running counter bumps when dequeueMessageFromKey is called for a CK message", @@ -264,8 +248,7 @@ describe("CK base-queue counters", () => { const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); const queueKey = testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, "ck-a"); const messageKey = testOptions.keys.messageKey(msg.orgId, msg.runId); - const runningCounterKey = - testOptions.keys.queueRunningCounterKeyFromQueue(queueKey); + const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(queueKey); await queue.redis.set( messageKey, @@ -292,9 +275,7 @@ describe("CK base-queue counters", () => { expect(Number(await queue.redis.get(runningCounterKey))).toBe(3); - const running = await queue.currentConcurrencyOfQueues(authenticatedEnvDev, [ - msg.queue, - ]); + const running = await queue.currentConcurrencyOfQueues(authenticatedEnvDev, [msg.queue]); expect(running[msg.queue]).toBe(3); } finally { await queue.quit(); @@ -302,64 +283,54 @@ describe("CK base-queue counters", () => { } ); - redisTest( - "floor-at-zero protects against spurious decrements", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const variantA = testOptions.keys.queueKey( - authenticatedEnvDev, - "task/my-task", - "ck-a" - ); - const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(variantA); - await queue.redis.set(runningCounterKey, "0"); - - // Call the Tracked release directly with un-prefixed keys (ioredis prepends the prefix) - await queue.redis.releaseConcurrencyTracked( - testOptions.keys.queueCurrentConcurrencyKeyFromQueue(variantA), - testOptions.keys.envCurrentConcurrencyKey(authenticatedEnvDev), - testOptions.keys.queueCurrentDequeuedKeyFromQueue(variantA), - testOptions.keys.envCurrentDequeuedKey(authenticatedEnvDev), - runningCounterKey, - testOptions.keys.ckIndexKeyFromQueue(variantA), - "phantom-message", - "runqueue:test:", - DEFAULT_COUNTER_TTL - ); - - expect(Number(await queue.redis.get(runningCounterKey))).toBe(0); - } finally { - await queue.quit(); - } + redisTest("floor-at-zero protects against spurious decrements", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const variantA = testOptions.keys.queueKey(authenticatedEnvDev, "task/my-task", "ck-a"); + const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(variantA); + await queue.redis.set(runningCounterKey, "0"); + + // Call the Tracked release directly with un-prefixed keys (ioredis prepends the prefix) + await queue.redis.releaseConcurrencyTracked( + testOptions.keys.queueCurrentConcurrencyKeyFromQueue(variantA), + testOptions.keys.envCurrentConcurrencyKey(authenticatedEnvDev), + testOptions.keys.queueCurrentDequeuedKeyFromQueue(variantA), + testOptions.keys.envCurrentDequeuedKey(authenticatedEnvDev), + runningCounterKey, + testOptions.keys.ckIndexKeyFromQueue(variantA), + "phantom-message", + "runqueue:test:", + DEFAULT_COUNTER_TTL + ); + + expect(Number(await queue.redis.get(runningCounterKey))).toBe(0); + } finally { + await queue.quit(); } - ); + }); - redisTest( - "lengthCounter has 24h TTL after lazy-init", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ runId: "r1", concurrencyKey: "ck-a" }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); + redisTest("lengthCounter has 24h TTL after lazy-init", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: "r1", concurrencyKey: "ck-a" }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); - const counterKey = testOptions.keys.queueLengthCounterKey( - authenticatedEnvDev, - "task/my-task" - ); - const ttl = await queue.redis.ttl(counterKey); - // Expect roughly 86400; allow only a few seconds of slack for test scheduling. - expect(ttl).toBeGreaterThanOrEqual(86390); - expect(ttl).toBeLessThanOrEqual(86400); - } finally { - await queue.quit(); - } + const counterKey = testOptions.keys.queueLengthCounterKey( + authenticatedEnvDev, + "task/my-task" + ); + const ttl = await queue.redis.ttl(counterKey); + // Expect roughly 86400; allow only a few seconds of slack for test scheduling. + expect(ttl).toBeGreaterThanOrEqual(86390); + expect(ttl).toBeLessThanOrEqual(86400); + } finally { + await queue.quit(); } - ); + }); redisTest( "duplicate CK enqueue (same runId) does not inflate lengthCounter", @@ -438,8 +409,7 @@ describe("CK base-queue counters", () => { const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); const queueKey = testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, "ck-a"); const messageKey = testOptions.keys.messageKey(msg.orgId, msg.runId); - const runningCounterKey = - testOptions.keys.queueRunningCounterKeyFromQueue(queueKey); + const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(queueKey); await queue.redis.set( messageKey, @@ -447,11 +417,19 @@ describe("CK base-queue counters", () => { ); // First call: SADD returns 1, runningCounter goes 0 -> 1. - await queue.redis.dequeueMessageFromKeyTracked(messageKey, "runqueue:test:", DEFAULT_COUNTER_TTL); + await queue.redis.dequeueMessageFromKeyTracked( + messageKey, + "runqueue:test:", + DEFAULT_COUNTER_TTL + ); expect(Number(await queue.redis.get(runningCounterKey))).toBe(1); // Second call on the same messageKey: SADD returns 0, runningCounter must stay at 1. - await queue.redis.dequeueMessageFromKeyTracked(messageKey, "runqueue:test:", DEFAULT_COUNTER_TTL); + await queue.redis.dequeueMessageFromKeyTracked( + messageKey, + "runqueue:test:", + DEFAULT_COUNTER_TTL + ); expect(Number(await queue.redis.get(runningCounterKey))).toBe(1); } finally { await queue.quit(); @@ -459,50 +437,47 @@ describe("CK base-queue counters", () => { } ); - redisTest( - "nack lazy-inits lengthCounter when it expired", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); - // Seed three messages on the CK variant so the lazy-init has a non-trivial floor. - for (let i = 0; i < 3; i++) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ runId: `seed-${i}`, concurrencyKey: "ck-a" }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - - // Simulate counter expiry (the 24h TTL kicked in). - const counterKey = testOptions.keys.queueLengthCounterKey( - authenticatedEnvDev, - "task/my-task" - ); - await queue.redis.del(counterKey); - expect(await queue.redis.exists(counterKey)).toBe(0); - - // Dequeue one to currentConcurrency so we have something to nack back. - const shard = testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2); - await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 1); - - // Nack a CK message. nackMessageCkTracked should lazy-init the counter - // (find 2 already in zset + 1 we're re-queuing) rather than starting from 1. - await queue.nackMessage({ - orgId: msg.orgId, - messageId: "seed-0", + redisTest("nack lazy-inits lengthCounter when it expired", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); + // Seed three messages on the CK variant so the lazy-init has a non-trivial floor. + for (let i = 0; i < 3; i++) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: `seed-${i}`, concurrencyKey: "ck-a" }), + workerQueue: authenticatedEnvDev.id, skipDequeueProcessing: true, }); - - // 3 originals, 1 was dequeued (still re-queued by nack), counter should now reflect all 3. - const observed = await queue.lengthOfQueue(authenticatedEnvDev, msg.queue); - expect(observed).toBe(3); - } finally { - await queue.quit(); } + + // Simulate counter expiry (the 24h TTL kicked in). + const counterKey = testOptions.keys.queueLengthCounterKey( + authenticatedEnvDev, + "task/my-task" + ); + await queue.redis.del(counterKey); + expect(await queue.redis.exists(counterKey)).toBe(0); + + // Dequeue one to currentConcurrency so we have something to nack back. + const shard = testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2); + await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 1); + + // Nack a CK message. nackMessageCkTracked should lazy-init the counter + // (find 2 already in zset + 1 we're re-queuing) rather than starting from 1. + await queue.nackMessage({ + orgId: msg.orgId, + messageId: "seed-0", + skipDequeueProcessing: true, + }); + + // 3 originals, 1 was dequeued (still re-queued by nack), counter should now reflect all 3. + const observed = await queue.lengthOfQueue(authenticatedEnvDev, msg.queue); + expect(observed).toBe(3); + } finally { + await queue.quit(); } - ); + }); redisTest( "fast-path-variant dequeue seeds runningCounter without missing its own SCARD", @@ -521,14 +496,9 @@ describe("CK base-queue counters", () => { msg.queue, "ck-fastpath" ); - const otherVariant = testOptions.keys.queueKey( - authenticatedEnvDev, - msg.queue, - "ck-other" - ); + const otherVariant = testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, "ck-other"); const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue(fastVariant); - const runningCounterKey = - testOptions.keys.queueRunningCounterKeyFromQueue(fastVariant); + const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(fastVariant); // Seed 3 prior fast-path runs into the fast variant's currentDequeued (no zset, no ckIndex). for (let i = 0; i < 3; i++) { @@ -549,7 +519,11 @@ describe("CK base-queue counters", () => { messageKey, JSON.stringify({ ...msg, queue: fastVariant, version: "2", workerQueue: "wq" }) ); - await queue.redis.dequeueMessageFromKeyTracked(messageKey, "runqueue:test:", DEFAULT_COUNTER_TTL); + await queue.redis.dequeueMessageFromKeyTracked( + messageKey, + "runqueue:test:", + DEFAULT_COUNTER_TTL + ); // True running across all variants: 3 (fast prior) + 1 (fast new) + 1 (other) = 5. // Without the ownVariantSeen fix, the seed would miss the fast variant entirely @@ -562,82 +536,76 @@ describe("CK base-queue counters", () => { } ); - redisTest( - "release lazy-inits runningCounter when missing", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); - const variant = testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, "ck-a"); - const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(variant); - - // Enqueue so ckIndex picks up the variant. - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - // Seed two running messages directly into currentDequeued so release has something to remove. - await queue.redis.sadd(`${variant}:currentDequeued`, msg.runId); - await queue.redis.sadd(`${variant}:currentDequeued`, "r2"); + redisTest("release lazy-inits runningCounter when missing", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); + const variant = testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, "ck-a"); + const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(variant); + + // Enqueue so ckIndex picks up the variant. + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + // Seed two running messages directly into currentDequeued so release has something to remove. + await queue.redis.sadd(`${variant}:currentDequeued`, msg.runId); + await queue.redis.sadd(`${variant}:currentDequeued`, "r2"); - // Counter is missing β€” simulates post-TTL state without waiting. - expect(await queue.redis.exists(runningCounterKey)).toBe(0); + // Counter is missing β€” simulates post-TTL state without waiting. + expect(await queue.redis.exists(runningCounterKey)).toBe(0); - // releaseAllConcurrency calls releaseConcurrencyTracked under the hood for CK queues. - await queue.releaseAllConcurrency(msg.orgId, msg.runId); + // releaseAllConcurrency calls releaseConcurrencyTracked under the hood for CK queues. + await queue.releaseAllConcurrency(msg.orgId, msg.runId); - // Pre-release truth was 2 in flight; after release one remains. Counter should be 1. - const counterVal = Number(await queue.redis.get(runningCounterKey)); - expect(counterVal).toBe(1); - } finally { - await queue.quit(); - } + // Pre-release truth was 2 in flight; after release one remains. Counter should be 1. + const counterVal = Number(await queue.redis.get(runningCounterKey)); + expect(counterVal).toBe(1); + } finally { + await queue.quit(); } - ); + }); - redisTest( - "counterTtlSeconds option is honored on lazy-init", - async ({ redisContainer }) => { - // Build a queue with a short TTL and verify the counter is SET with it. - const queue = new RunQueue({ - ...testOptions, - counterTtlSeconds: 60, - queueSelectionStrategy: new FairQueueSelectionStrategy({ - redis: { - keyPrefix: "runqueue:test:", - host: redisContainer.getHost(), - port: redisContainer.getPort(), - }, - keys: testOptions.keys, - }), + redisTest("counterTtlSeconds option is honored on lazy-init", async ({ redisContainer }) => { + // Build a queue with a short TTL and verify the counter is SET with it. + const queue = new RunQueue({ + ...testOptions, + counterTtlSeconds: 60, + 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(), + }, + }); + try { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: "r1", concurrencyKey: "ck-a" }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, }); - try { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ runId: "r1", concurrencyKey: "ck-a" }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - const counterKey = testOptions.keys.queueLengthCounterKey( - authenticatedEnvDev, - "task/my-task" - ); - const ttl = await queue.redis.ttl(counterKey); - // Generous lower bound β€” CI workers can occasionally stall multiple - // seconds between the lazy-init SET and the TTL read. - expect(ttl).toBeGreaterThanOrEqual(50); - expect(ttl).toBeLessThanOrEqual(60); - } finally { - await queue.quit(); - } + const counterKey = testOptions.keys.queueLengthCounterKey( + authenticatedEnvDev, + "task/my-task" + ); + const ttl = await queue.redis.ttl(counterKey); + // Generous lower bound β€” CI workers can occasionally stall multiple + // seconds between the lazy-init SET and the TTL read. + expect(ttl).toBeGreaterThanOrEqual(50); + expect(ttl).toBeLessThanOrEqual(60); + } finally { + await queue.quit(); } - ); + }); }); diff --git a/internal-packages/run-engine/src/run-queue/tests/ckIndex.test.ts b/internal-packages/run-engine/src/run-queue/tests/ckIndex.test.ts index 3945b455a0..5d468bf0eb 100644 --- a/internal-packages/run-engine/src/run-queue/tests/ckIndex.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/ckIndex.test.ts @@ -97,12 +97,7 @@ describe("CK Index", () => { const masterQueueKey = testOptions.keys.masterQueueKeyForShard( testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2) ); - const masterMembers = await queue.redis.zrange( - masterQueueKey, - 0, - -1, - "WITHSCORES" - ); + const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1, "WITHSCORES"); // Should have exactly one member ending with :ck:* const ckWildcardMembers = masterMembers.filter( (m, i) => i % 2 === 0 && m.endsWith(":ck:*") @@ -127,264 +122,232 @@ describe("CK Index", () => { } ); - redisTest( - "multiple CKs result in single master queue entry", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now(); - const msg1 = makeMessage({ - runId: "r1", - concurrencyKey: "ck-a", - timestamp: now, - }); - const msg2 = makeMessage({ - runId: "r2", - concurrencyKey: "ck-b", - timestamp: now + 100, - }); - const msg3 = makeMessage({ - runId: "r3", - concurrencyKey: "ck-c", - timestamp: now + 200, + redisTest("multiple CKs result in single master queue entry", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now(); + const msg1 = makeMessage({ + runId: "r1", + concurrencyKey: "ck-a", + timestamp: now, + }); + const msg2 = makeMessage({ + runId: "r2", + concurrencyKey: "ck-b", + timestamp: now + 100, + }); + const msg3 = makeMessage({ + runId: "r3", + concurrencyKey: "ck-c", + timestamp: now + 200, + }); + + for (const msg of [msg1, msg2, msg3]) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, }); - - for (const msg of [msg1, msg2, msg3]) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - - // Master queue should have exactly ONE entry (the :ck:* wildcard) - const masterQueueKey = testOptions.keys.masterQueueKeyForShard( - testOptions.keys.masterQueueShardForEnvironment(msg1.environmentId, 2) - ); - const masterMembers = await queue.redis.zrange( - masterQueueKey, - 0, - -1 - ); - // Filter to only members for our queue - const ourMembers = masterMembers.filter((m) => - m.includes("queue:task/my-task") - ); - expect(ourMembers.length).toBe(1); - expect(ourMembers[0]).toContain(":ck:*"); - - // CK index should have 3 entries - const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( - testOptions.keys.queueKey(authenticatedEnvDev, msg1.queue, msg1.concurrencyKey) - ); - const ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); - expect(ckIndexMembers.length).toBe(3); - } finally { - await queue.quit(); } - } - ); - - redisTest( - "dequeue from CK queue distributes across sub-queues", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; // In the past so they're ready - const msg1 = makeMessage({ - runId: "r1", - concurrencyKey: "ck-a", - timestamp: now, - }); - const msg2 = makeMessage({ - runId: "r2", - concurrencyKey: "ck-b", - timestamp: now + 1, - }); - const msg3 = makeMessage({ - runId: "r3", - concurrencyKey: "ck-a", - timestamp: now + 2, - }); - - for (const msg of [msg1, msg2, msg3]) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - // Dequeue via the master queue consumer - const shard = testOptions.keys.masterQueueShardForEnvironment( - msg1.environmentId, - 2 - ); - const messages = await queue.testDequeueFromMasterQueue(shard, msg1.environmentId, 10); - - // Should dequeue messages from both CK sub-queues - expect(messages).toBeDefined(); - // We should get at least 2 messages (one from each CK) - // The exact order depends on CK index scoring - expect(messages!.length).toBeGreaterThanOrEqual(2); - - const dequeuedRunIds = messages!.map((m: any) => m.messageId); - // r1 (ck-a, oldest) and r2 (ck-b) should be dequeued - expect(dequeuedRunIds).toContain("r1"); - expect(dequeuedRunIds).toContain("r2"); - } finally { - await queue.quit(); - } + // Master queue should have exactly ONE entry (the :ck:* wildcard) + const masterQueueKey = testOptions.keys.masterQueueKeyForShard( + testOptions.keys.masterQueueShardForEnvironment(msg1.environmentId, 2) + ); + const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); + // Filter to only members for our queue + const ourMembers = masterMembers.filter((m) => m.includes("queue:task/my-task")); + expect(ourMembers.length).toBe(1); + expect(ourMembers[0]).toContain(":ck:*"); + + // CK index should have 3 entries + const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( + testOptions.keys.queueKey(authenticatedEnvDev, msg1.queue, msg1.concurrencyKey) + ); + const ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); + expect(ckIndexMembers.length).toBe(3); + } finally { + await queue.quit(); } - ); + }); - redisTest( - "empty CK sub-queue is removed from CK index", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; - const msg1 = makeMessage({ - runId: "r1", - concurrencyKey: "ck-a", - timestamp: now, - }); - const msg2 = makeMessage({ - runId: "r2", - concurrencyKey: "ck-b", - timestamp: now + 1, + redisTest("dequeue from CK queue distributes across sub-queues", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; // In the past so they're ready + const msg1 = makeMessage({ + runId: "r1", + concurrencyKey: "ck-a", + timestamp: now, + }); + const msg2 = makeMessage({ + runId: "r2", + concurrencyKey: "ck-b", + timestamp: now + 1, + }); + const msg3 = makeMessage({ + runId: "r3", + concurrencyKey: "ck-a", + timestamp: now + 2, + }); + + for (const msg of [msg1, msg2, msg3]) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, }); - - for (const msg of [msg1, msg2]) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - - // CK index should have 2 entries initially - const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( - testOptions.keys.queueKey(authenticatedEnvDev, msg1.queue, msg1.concurrencyKey) - ); - let ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); - expect(ckIndexMembers.length).toBe(2); - - // Dequeue both messages - const shard = testOptions.keys.masterQueueShardForEnvironment( - msg1.environmentId, - 2 - ); - await queue.testDequeueFromMasterQueue(shard, msg1.environmentId, 10); - - // CK index should be empty (both sub-queues drained) - ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); - expect(ckIndexMembers.length).toBe(0); - } finally { - await queue.quit(); } - } - ); - redisTest( - "empty CK index removes :ck:* from master queue", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; - const msg = makeMessage({ - runId: "r1", - concurrencyKey: "ck-a", - timestamp: now, - }); + // Dequeue via the master queue consumer + const shard = testOptions.keys.masterQueueShardForEnvironment(msg1.environmentId, 2); + const messages = await queue.testDequeueFromMasterQueue(shard, msg1.environmentId, 10); + + // Should dequeue messages from both CK sub-queues + expect(messages).toBeDefined(); + // We should get at least 2 messages (one from each CK) + // The exact order depends on CK index scoring + expect(messages!.length).toBeGreaterThanOrEqual(2); + + const dequeuedRunIds = messages!.map((m: any) => m.messageId); + // r1 (ck-a, oldest) and r2 (ck-b) should be dequeued + expect(dequeuedRunIds).toContain("r1"); + expect(dequeuedRunIds).toContain("r2"); + } finally { + await queue.quit(); + } + }); + redisTest("empty CK sub-queue is removed from CK index", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; + const msg1 = makeMessage({ + runId: "r1", + concurrencyKey: "ck-a", + timestamp: now, + }); + const msg2 = makeMessage({ + runId: "r2", + concurrencyKey: "ck-b", + timestamp: now + 1, + }); + + for (const msg of [msg1, msg2]) { await queue.enqueueMessage({ env: authenticatedEnvDev, message: msg, workerQueue: authenticatedEnvDev.id, skipDequeueProcessing: true, }); - - const masterQueueKey = testOptions.keys.masterQueueKeyForShard( - testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2) - ); - - // Master queue should have :ck:* entry - let masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); - expect(masterMembers.length).toBe(1); - - // Dequeue the message - const shard = testOptions.keys.masterQueueShardForEnvironment( - msg.environmentId, - 2 - ); - await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 10); - - // Master queue should be empty - masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); - expect(masterMembers.length).toBe(0); - } finally { - await queue.quit(); } - } - ); - redisTest( - "mixed CK and non-CK queues in same shard", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; + // CK index should have 2 entries initially + const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( + testOptions.keys.queueKey(authenticatedEnvDev, msg1.queue, msg1.concurrencyKey) + ); + let ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); + expect(ckIndexMembers.length).toBe(2); + + // Dequeue both messages + const shard = testOptions.keys.masterQueueShardForEnvironment(msg1.environmentId, 2); + await queue.testDequeueFromMasterQueue(shard, msg1.environmentId, 10); + + // CK index should be empty (both sub-queues drained) + ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); + expect(ckIndexMembers.length).toBe(0); + } finally { + await queue.quit(); + } + }); - // Non-CK message - const msgNoCk = makeMessage({ - runId: "r-no-ck", - timestamp: now, - }); + redisTest("empty CK index removes :ck:* from master queue", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; + const msg = makeMessage({ + runId: "r1", + concurrencyKey: "ck-a", + timestamp: now, + }); + + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + + const masterQueueKey = testOptions.keys.masterQueueKeyForShard( + testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2) + ); + + // Master queue should have :ck:* entry + let masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); + expect(masterMembers.length).toBe(1); + + // Dequeue the message + const shard = testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2); + await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 10); + + // Master queue should be empty + masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); + expect(masterMembers.length).toBe(0); + } finally { + await queue.quit(); + } + }); - // CK messages - const msgCk1 = makeMessage({ - runId: "r-ck-1", - concurrencyKey: "ck-a", - timestamp: now + 1, - }); - const msgCk2 = makeMessage({ - runId: "r-ck-2", - concurrencyKey: "ck-b", - timestamp: now + 2, + redisTest("mixed CK and non-CK queues in same shard", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; + + // Non-CK message + const msgNoCk = makeMessage({ + runId: "r-no-ck", + timestamp: now, + }); + + // CK messages + const msgCk1 = makeMessage({ + runId: "r-ck-1", + concurrencyKey: "ck-a", + timestamp: now + 1, + }); + const msgCk2 = makeMessage({ + runId: "r-ck-2", + concurrencyKey: "ck-b", + timestamp: now + 2, + }); + + for (const msg of [msgNoCk, msgCk1, msgCk2]) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, }); - - for (const msg of [msgNoCk, msgCk1, msgCk2]) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - - // Master queue should have 2 entries: one non-CK queue and one :ck:* - const masterQueueKey = testOptions.keys.masterQueueKeyForShard( - testOptions.keys.masterQueueShardForEnvironment(msgNoCk.environmentId, 2) - ); - const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); - expect(masterMembers.length).toBe(2); - - // One should be the non-CK queue, one should be :ck:* - const ckWildcard = masterMembers.filter((m) => m.endsWith(":ck:*")); - const nonCk = masterMembers.filter( - (m) => !m.includes(":ck:") - ); - expect(ckWildcard.length).toBe(1); - expect(nonCk.length).toBe(1); - } finally { - await queue.quit(); } + + // Master queue should have 2 entries: one non-CK queue and one :ck:* + const masterQueueKey = testOptions.keys.masterQueueKeyForShard( + testOptions.keys.masterQueueShardForEnvironment(msgNoCk.environmentId, 2) + ); + const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); + expect(masterMembers.length).toBe(2); + + // One should be the non-CK queue, one should be :ck:* + const ckWildcard = masterMembers.filter((m) => m.endsWith(":ck:*")); + const nonCk = masterMembers.filter((m) => !m.includes(":ck:")); + expect(ckWildcard.length).toBe(1); + expect(nonCk.length).toBe(1); + } finally { + await queue.quit(); } - ); + }); redisTest( "acknowledge CK message rebalances CK index and master queue", @@ -413,10 +376,7 @@ describe("CK Index", () => { } // Dequeue one message - const shard = testOptions.keys.masterQueueShardForEnvironment( - msg1.environmentId, - 2 - ); + const shard = testOptions.keys.masterQueueShardForEnvironment(msg1.environmentId, 2); const messages = await queue.testDequeueFromMasterQueue(shard, msg1.environmentId, 1); expect(messages!.length).toBe(1); expect(messages![0].messageId).toBe("r1"); @@ -459,63 +419,57 @@ describe("CK Index", () => { } ); - redisTest( - "nack CK message rebalances CK index", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; - const msg = makeMessage({ - runId: "r1", - concurrencyKey: "ck-a", - timestamp: now, - }); - - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - - // Dequeue the message - const shard = testOptions.keys.masterQueueShardForEnvironment( - msg.environmentId, - 2 - ); - const messages = await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 1); - expect(messages!.length).toBe(1); - - // Nack the message (re-enqueue) - await queue.nackMessage({ - orgId: msg.orgId, - messageId: "r1", - retryAt: Date.now() + 5000, - incrementAttemptCount: false, - skipDequeueProcessing: true, - }); - - // CK index should have the ck-a entry (message re-enqueued) - const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( - testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, msg.concurrencyKey) - ); - const ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); - expect(ckIndexMembers.length).toBe(1); - - // Master queue should have the :ck:* entry - const masterQueueKey = testOptions.keys.masterQueueKeyForShard(shard); - const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); - expect(masterMembers.length).toBe(1); - expect(masterMembers[0]).toContain(":ck:*"); - - // No old-format entries - const oldFormatMembers = masterMembers.filter( - (m) => m.includes(":ck:") && !m.endsWith(":ck:*") - ); - expect(oldFormatMembers.length).toBe(0); - } finally { - await queue.quit(); - } + redisTest("nack CK message rebalances CK index", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; + const msg = makeMessage({ + runId: "r1", + concurrencyKey: "ck-a", + timestamp: now, + }); + + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + + // Dequeue the message + const shard = testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2); + const messages = await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 1); + expect(messages!.length).toBe(1); + + // Nack the message (re-enqueue) + await queue.nackMessage({ + orgId: msg.orgId, + messageId: "r1", + retryAt: Date.now() + 5000, + incrementAttemptCount: false, + skipDequeueProcessing: true, + }); + + // CK index should have the ck-a entry (message re-enqueued) + const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( + testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, msg.concurrencyKey) + ); + const ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); + expect(ckIndexMembers.length).toBe(1); + + // Master queue should have the :ck:* entry + const masterQueueKey = testOptions.keys.masterQueueKeyForShard(shard); + const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); + expect(masterMembers.length).toBe(1); + expect(masterMembers[0]).toContain(":ck:*"); + + // No old-format entries + const oldFormatMembers = masterMembers.filter( + (m) => m.includes(":ck:") && !m.endsWith(":ck:*") + ); + expect(oldFormatMembers.length).toBe(0); + } finally { + await queue.quit(); } - ); + }); }); diff --git a/internal-packages/run-engine/src/run-queue/tests/dequeueMessageFromWorkerQueue.test.ts b/internal-packages/run-engine/src/run-queue/tests/dequeueMessageFromWorkerQueue.test.ts index cc7485483a..9138215bbe 100644 --- a/internal-packages/run-engine/src/run-queue/tests/dequeueMessageFromWorkerQueue.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/dequeueMessageFromWorkerQueue.test.ts @@ -272,9 +272,8 @@ describe("RunQueue.dequeueMessageFromWorkerQueue", () => { const dequeued3 = await queue.dequeueMessageFromWorkerQueue("test_12345", "main"); expect(dequeued3).toBeUndefined(); - const envConcurrencyAfter = await queue.currentConcurrencyOfEnvironment( - authenticatedEnvDev - ); + const envConcurrencyAfter = + await queue.currentConcurrencyOfEnvironment(authenticatedEnvDev); expect(envConcurrencyAfter).toBe(2); } finally { await queue.quit(); diff --git a/internal-packages/run-engine/src/run-queue/tests/enqueueMessage.test.ts b/internal-packages/run-engine/src/run-queue/tests/enqueueMessage.test.ts index 8ce2d68d5d..e45c99b25c 100644 --- a/internal-packages/run-engine/src/run-queue/tests/enqueueMessage.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/enqueueMessage.test.ts @@ -48,7 +48,10 @@ const messageDev: InputPayload = { vi.setConfig({ testTimeout: 60_000 }); -function createQueue(redisContainer: { getHost: () => string; getPort: () => number }, prefix = "runqueue:test:") { +function createQueue( + redisContainer: { getHost: () => string; getPort: () => number }, + prefix = "runqueue:test:" +) { return new RunQueue({ ...testOptions, queueSelectionStrategy: new FairQueueSelectionStrategy({ @@ -133,45 +136,48 @@ describe("RunQueue.enqueueMessage", () => { }); describe("RunQueue.enqueueMessage fast path", () => { - redisTest("should fast-path to worker queue when queue is empty and concurrency available", async ({ redisContainer }) => { - const queue = createQueue(redisContainer, "runqueue:fp1:"); - - try { - // Set concurrency limits - await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); - - // Enqueue with fast path enabled - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: messageDev, - workerQueue: authenticatedEnvDev.id, - enableFastPath: true, - }); - - // Queue sorted set should be empty (fast path skips it) - const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); - expect(queueLength).toBe(0); - - // Queue concurrency should be claimed (operational concurrency) - const queueConcurrency = await queue.currentConcurrencyOfQueue( - authenticatedEnvDev, - messageDev.queue - ); - expect(queueConcurrency).toBe(1); - - // Message should be directly in worker queue - dequeue it - const dequeued = await queue.dequeueMessageFromWorkerQueue( - "test_12345", - authenticatedEnvDev.id, - { blockingPop: false } - ); - assertNonNullable(dequeued); - expect(dequeued.messageId).toEqual(messageDev.runId); - expect(dequeued.message.version).toEqual("2"); - } finally { - await queue.quit(); + redisTest( + "should fast-path to worker queue when queue is empty and concurrency available", + async ({ redisContainer }) => { + const queue = createQueue(redisContainer, "runqueue:fp1:"); + + try { + // Set concurrency limits + await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); + + // Enqueue with fast path enabled + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: messageDev, + workerQueue: authenticatedEnvDev.id, + enableFastPath: true, + }); + + // Queue sorted set should be empty (fast path skips it) + const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); + expect(queueLength).toBe(0); + + // Queue concurrency should be claimed (operational concurrency) + const queueConcurrency = await queue.currentConcurrencyOfQueue( + authenticatedEnvDev, + messageDev.queue + ); + expect(queueConcurrency).toBe(1); + + // Message should be directly in worker queue - dequeue it + const dequeued = await queue.dequeueMessageFromWorkerQueue( + "test_12345", + authenticatedEnvDev.id, + { blockingPop: false } + ); + assertNonNullable(dequeued); + expect(dequeued.messageId).toEqual(messageDev.runId); + expect(dequeued.message.version).toEqual("2"); + } finally { + await queue.quit(); + } } - }); + ); redisTest("should take slow path when enableFastPath is false", async ({ redisContainer }) => { const queue = createQueue(redisContainer, "runqueue:fp2:"); @@ -201,105 +207,111 @@ describe("RunQueue.enqueueMessage fast path", () => { } }); - redisTest("should take slow path when queue has available messages", async ({ redisContainer }) => { - const queue = createQueue(redisContainer, "runqueue:fp3:"); - - try { - await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); - - // Enqueue a first message (slow path to populate the queue) - const message1: InputPayload = { - ...messageDev, - runId: "r1111", - timestamp: Date.now() - 1000, // in the past, so it's "available" - }; - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: message1, - workerQueue: authenticatedEnvDev.id, - enableFastPath: false, - }); - - // Now enqueue a second message with fast path - const message2: InputPayload = { - ...messageDev, - runId: "r2222", - timestamp: Date.now(), - }; - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: message2, - workerQueue: authenticatedEnvDev.id, - enableFastPath: true, - }); - - // Both messages should be in the queue sorted set (slow path for both) - const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); - expect(queueLength).toBe(2); - } finally { - await queue.quit(); + redisTest( + "should take slow path when queue has available messages", + async ({ redisContainer }) => { + const queue = createQueue(redisContainer, "runqueue:fp3:"); + + try { + await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); + + // Enqueue a first message (slow path to populate the queue) + const message1: InputPayload = { + ...messageDev, + runId: "r1111", + timestamp: Date.now() - 1000, // in the past, so it's "available" + }; + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: message1, + workerQueue: authenticatedEnvDev.id, + enableFastPath: false, + }); + + // Now enqueue a second message with fast path + const message2: InputPayload = { + ...messageDev, + runId: "r2222", + timestamp: Date.now(), + }; + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: message2, + workerQueue: authenticatedEnvDev.id, + enableFastPath: true, + }); + + // Both messages should be in the queue sorted set (slow path for both) + const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); + expect(queueLength).toBe(2); + } finally { + await queue.quit(); + } } - }); - - redisTest("should fast-path when queue only has future-scored messages", async ({ redisContainer }) => { - const queue = createQueue(redisContainer, "runqueue:fp4:"); - - try { - await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); - - // Enqueue a message with a future timestamp (simulating a nacked retry) - const futureMessage: InputPayload = { - ...messageDev, - runId: "r_future", - timestamp: Date.now() + 60_000, // 60 seconds in the future - }; - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: futureMessage, - workerQueue: authenticatedEnvDev.id, - enableFastPath: false, - }); - - // Queue has 1 message but it's not available (future score) - const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); - expect(queueLength).toBe(1); - - // Now enqueue a new message with fast path - const newMessage: InputPayload = { - ...messageDev, - runId: "r_new", - timestamp: Date.now(), - }; - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: newMessage, - workerQueue: authenticatedEnvDev.id, - enableFastPath: true, - }); - - // The future message stays in queue, new message went to worker queue - const queueLength2 = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); - expect(queueLength2).toBe(1); // Only the future message - - // Queue concurrency claimed for the fast-pathed message - const queueConcurrency = await queue.currentConcurrencyOfQueue( - authenticatedEnvDev, - messageDev.queue - ); - expect(queueConcurrency).toBe(1); - - // Can dequeue the fast-pathed message from worker queue - const dequeued = await queue.dequeueMessageFromWorkerQueue( - "test_12345", - authenticatedEnvDev.id, - { blockingPop: false } - ); - assertNonNullable(dequeued); - expect(dequeued.messageId).toEqual("r_new"); - } finally { - await queue.quit(); + ); + + redisTest( + "should fast-path when queue only has future-scored messages", + async ({ redisContainer }) => { + const queue = createQueue(redisContainer, "runqueue:fp4:"); + + try { + await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); + + // Enqueue a message with a future timestamp (simulating a nacked retry) + const futureMessage: InputPayload = { + ...messageDev, + runId: "r_future", + timestamp: Date.now() + 60_000, // 60 seconds in the future + }; + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: futureMessage, + workerQueue: authenticatedEnvDev.id, + enableFastPath: false, + }); + + // Queue has 1 message but it's not available (future score) + const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); + expect(queueLength).toBe(1); + + // Now enqueue a new message with fast path + const newMessage: InputPayload = { + ...messageDev, + runId: "r_new", + timestamp: Date.now(), + }; + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: newMessage, + workerQueue: authenticatedEnvDev.id, + enableFastPath: true, + }); + + // The future message stays in queue, new message went to worker queue + const queueLength2 = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); + expect(queueLength2).toBe(1); // Only the future message + + // Queue concurrency claimed for the fast-pathed message + const queueConcurrency = await queue.currentConcurrencyOfQueue( + authenticatedEnvDev, + messageDev.queue + ); + expect(queueConcurrency).toBe(1); + + // Can dequeue the fast-pathed message from worker queue + const dequeued = await queue.dequeueMessageFromWorkerQueue( + "test_12345", + authenticatedEnvDev.id, + { blockingPop: false } + ); + assertNonNullable(dequeued); + expect(dequeued.messageId).toEqual("r_new"); + } finally { + await queue.quit(); + } } - }); + ); redisTest("should take slow path when env concurrency is full", async ({ redisContainer }) => { // Use a low concurrency limit diff --git a/internal-packages/run-engine/src/run-queue/tests/keyProducer.test.ts b/internal-packages/run-engine/src/run-queue/tests/keyProducer.test.ts index 0f65b02088..3e31085d67 100644 --- a/internal-packages/run-engine/src/run-queue/tests/keyProducer.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/keyProducer.test.ts @@ -417,8 +417,12 @@ describe("KeyProducer", () => { it("isCkWildcard", () => { const keyProducer = new RunQueueFullKeyProducer(); - expect(keyProducer.isCkWildcard("{org:o1234}:proj:p1234:env:e1234:queue:task/foo:ck:*")).toBe(true); - expect(keyProducer.isCkWildcard("{org:o1234}:proj:p1234:env:e1234:queue:task/foo:ck:bar")).toBe(false); + expect(keyProducer.isCkWildcard("{org:o1234}:proj:p1234:env:e1234:queue:task/foo:ck:*")).toBe( + true + ); + expect(keyProducer.isCkWildcard("{org:o1234}:proj:p1234:env:e1234:queue:task/foo:ck:bar")).toBe( + false + ); expect(keyProducer.isCkWildcard("{org:o1234}:proj:p1234:env:e1234:queue:task/foo")).toBe(false); }); diff --git a/internal-packages/run-engine/src/run-queue/tests/nack.test.ts b/internal-packages/run-engine/src/run-queue/tests/nack.test.ts index 8bfd8b3a76..a1cf274017 100644 --- a/internal-packages/run-engine/src/run-queue/tests/nack.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/nack.test.ts @@ -89,9 +89,8 @@ describe("RunQueue.nackMessage", () => { ); expect(queueCurrentConcurrency).toBe(1); - const envCurrentConcurrency = await queue.currentConcurrencyOfEnvironment( - authenticatedEnvDev - ); + const envCurrentConcurrency = + await queue.currentConcurrencyOfEnvironment(authenticatedEnvDev); expect(envCurrentConcurrency).toBe(1); // Nack the message @@ -107,9 +106,8 @@ describe("RunQueue.nackMessage", () => { ); expect(queueCurrentConcurrencyAfterNack).toBe(0); - const envCurrentConcurrencyAfterNack = await queue.currentConcurrencyOfEnvironment( - authenticatedEnvDev - ); + const envCurrentConcurrencyAfterNack = + await queue.currentConcurrencyOfEnvironment(authenticatedEnvDev); expect(envCurrentConcurrencyAfterNack).toBe(0); const envQueueLength = await queue.lengthOfEnvQueue(authenticatedEnvDev); @@ -196,9 +194,8 @@ describe("RunQueue.nackMessage", () => { const envQueueLengthDequeue = await queue.lengthOfEnvQueue(authenticatedEnvDev); expect(envQueueLengthDequeue).toBe(0); - const deadLetterQueueLengthBefore = await queue.lengthOfDeadLetterQueue( - authenticatedEnvDev - ); + const deadLetterQueueLengthBefore = + await queue.lengthOfDeadLetterQueue(authenticatedEnvDev); expect(deadLetterQueueLengthBefore).toBe(0); await queue.nackMessage({ @@ -209,9 +206,8 @@ describe("RunQueue.nackMessage", () => { const envQueueLengthAfterNack = await queue.lengthOfEnvQueue(authenticatedEnvDev); expect(envQueueLengthAfterNack).toBe(0); - const deadLetterQueueLengthAfterNack = await queue.lengthOfDeadLetterQueue( - authenticatedEnvDev - ); + const deadLetterQueueLengthAfterNack = + await queue.lengthOfDeadLetterQueue(authenticatedEnvDev); expect(deadLetterQueueLengthAfterNack).toBe(1); } finally { await queue.quit(); diff --git a/internal-packages/run-store/src/NoopRunStore.ts b/internal-packages/run-store/src/NoopRunStore.ts index e27080c9af..067aa3de09 100644 --- a/internal-packages/run-store/src/NoopRunStore.ts +++ b/internal-packages/run-store/src/NoopRunStore.ts @@ -5,31 +5,85 @@ export class NoopRunStore implements RunStore { private fail(method: string): never { throw new Error(`NoopRunStore.${method} called`); } - createRun(): never { return this.fail("createRun"); } - createCancelledRun(): never { return this.fail("createCancelledRun"); } - createFailedRun(): never { return this.fail("createFailedRun"); } - startAttempt(): never { return this.fail("startAttempt"); } - completeAttemptSuccess(): never { return this.fail("completeAttemptSuccess"); } - recordRetryOutcome(): never { return this.fail("recordRetryOutcome"); } - requeueRun(): never { return this.fail("requeueRun"); } - recordBulkActionMembership(): never { return this.fail("recordBulkActionMembership"); } - cancelRun(): never { return this.fail("cancelRun"); } - failRunPermanently(): never { return this.fail("failRunPermanently"); } - expireRun(): never { return this.fail("expireRun"); } - expireRunsBatch(): never { return this.fail("expireRunsBatch"); } - lockRunToWorker(): never { return this.fail("lockRunToWorker"); } - parkPendingVersion(): never { return this.fail("parkPendingVersion"); } - promotePendingVersionRuns(): never { return this.fail("promotePendingVersionRuns"); } - suspendForCheckpoint(): never { return this.fail("suspendForCheckpoint"); } - resumeFromCheckpoint(): never { return this.fail("resumeFromCheckpoint"); } - rescheduleRun(): never { return this.fail("rescheduleRun"); } - enqueueDelayedRun(): never { return this.fail("enqueueDelayedRun"); } - rewriteDebouncedRun(): never { return this.fail("rewriteDebouncedRun"); } - updateMetadata(): never { return this.fail("updateMetadata"); } - clearIdempotencyKey(): never { return this.fail("clearIdempotencyKey"); } - pushTags(): never { return this.fail("pushTags"); } - pushRealtimeStream(): never { return this.fail("pushRealtimeStream"); } - findRun(): never { return this.fail("findRun"); } - findRunOrThrow(): never { return this.fail("findRunOrThrow"); } - findRuns(): never { return this.fail("findRuns"); } + createRun(): never { + return this.fail("createRun"); + } + createCancelledRun(): never { + return this.fail("createCancelledRun"); + } + createFailedRun(): never { + return this.fail("createFailedRun"); + } + startAttempt(): never { + return this.fail("startAttempt"); + } + completeAttemptSuccess(): never { + return this.fail("completeAttemptSuccess"); + } + recordRetryOutcome(): never { + return this.fail("recordRetryOutcome"); + } + requeueRun(): never { + return this.fail("requeueRun"); + } + recordBulkActionMembership(): never { + return this.fail("recordBulkActionMembership"); + } + cancelRun(): never { + return this.fail("cancelRun"); + } + failRunPermanently(): never { + return this.fail("failRunPermanently"); + } + expireRun(): never { + return this.fail("expireRun"); + } + expireRunsBatch(): never { + return this.fail("expireRunsBatch"); + } + lockRunToWorker(): never { + return this.fail("lockRunToWorker"); + } + parkPendingVersion(): never { + return this.fail("parkPendingVersion"); + } + promotePendingVersionRuns(): never { + return this.fail("promotePendingVersionRuns"); + } + suspendForCheckpoint(): never { + return this.fail("suspendForCheckpoint"); + } + resumeFromCheckpoint(): never { + return this.fail("resumeFromCheckpoint"); + } + rescheduleRun(): never { + return this.fail("rescheduleRun"); + } + enqueueDelayedRun(): never { + return this.fail("enqueueDelayedRun"); + } + rewriteDebouncedRun(): never { + return this.fail("rewriteDebouncedRun"); + } + updateMetadata(): never { + return this.fail("updateMetadata"); + } + clearIdempotencyKey(): never { + return this.fail("clearIdempotencyKey"); + } + pushTags(): never { + return this.fail("pushTags"); + } + pushRealtimeStream(): never { + return this.fail("pushRealtimeStream"); + } + findRun(): never { + return this.fail("findRun"); + } + findRunOrThrow(): never { + return this.fail("findRunOrThrow"); + } + findRuns(): never { + return this.fail("findRuns"); + } } diff --git a/internal-packages/run-store/src/PostgresRunStore.test.ts b/internal-packages/run-store/src/PostgresRunStore.test.ts index 47876b70c8..49fcbfe450 100644 --- a/internal-packages/run-store/src/PostgresRunStore.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.test.ts @@ -77,38 +77,41 @@ function buildCreateRunInput(params: { } describe("PostgresRunStore", () => { - postgresTest("createRun creates the run with one snapshot and no waitpoint", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "createRun creates the run with one snapshot and no waitpoint", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ - prisma, - // The read-only client just needs to be a PrismaClient for these tests. - readOnlyPrisma: prisma, - }); + const store = new PostgresRunStore({ + prisma, + // The read-only client just needs to be a PrismaClient for these tests. + readOnlyPrisma: prisma, + }); - const runId = "run_test_1"; + const runId = "run_test_1"; - const run = await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + const run = await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - expect(run.id).toBe(runId); - expect(run.status).toBe("PENDING"); - expect(run.associatedWaitpoint).toBeNull(); + expect(run.id).toBe(runId); + expect(run.status).toBe("PENDING"); + expect(run.associatedWaitpoint).toBeNull(); - const snapshots = await prisma.taskRunExecutionSnapshot.findMany({ - where: { runId }, - }); + const snapshots = await prisma.taskRunExecutionSnapshot.findMany({ + where: { runId }, + }); - expect(snapshots).toHaveLength(1); - expect(snapshots[0]?.executionStatus).toBe("RUN_CREATED"); - expect(snapshots[0]?.runStatus).toBe("PENDING"); - }); + expect(snapshots).toHaveLength(1); + expect(snapshots[0]?.executionStatus).toBe("RUN_CREATED"); + expect(snapshots[0]?.runStatus).toBe("PENDING"); + } + ); postgresTest( "createCancelledRun creates a CANCELED run with one FINISHED/CANCELED execution snapshot", @@ -233,35 +236,46 @@ describe("PostgresRunStore", () => { } ); - postgresTest("startAttempt sets status to EXECUTING and records attempt fields", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "startAttempt sets status to EXECUTING and records attempt fields", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_start_attempt_1"; + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_start_attempt_1"; - await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - const executedAt = new Date("2026-03-01T10:00:00.000Z"); + const executedAt = new Date("2026-03-01T10:00:00.000Z"); - const run = await store.startAttempt( - runId, - { attemptNumber: 1, executedAt, isWarmStart: true }, - { select: { id: true, status: true, attemptNumber: true, executedAt: true, isWarmStart: true } } - ); + const run = await store.startAttempt( + runId, + { attemptNumber: 1, executedAt, isWarmStart: true }, + { + select: { + id: true, + status: true, + attemptNumber: true, + executedAt: true, + isWarmStart: true, + }, + } + ); - expect(run.id).toBe(runId); - expect(run.status).toBe("EXECUTING"); - expect(run.attemptNumber).toBe(1); - expect(run.executedAt).toEqual(executedAt); - expect(run.isWarmStart).toBe(true); - }); + expect(run.id).toBe(runId); + expect(run.status).toBe("EXECUTING"); + expect(run.attemptNumber).toBe(1); + expect(run.executedAt).toEqual(executedAt); + expect(run.isWarmStart).toBe(true); + } + ); postgresTest( "completeAttemptSuccess sets status to COMPLETED_SUCCESSFULLY and creates a FINISHED snapshot", @@ -330,36 +344,43 @@ describe("PostgresRunStore", () => { } ); - postgresTest("recordRetryOutcome updates machine/usage/cost but leaves status unchanged", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "recordRetryOutcome updates machine/usage/cost but leaves status unchanged", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_retry_outcome_1"; + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_retry_outcome_1"; - await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - // Set status to EXECUTING first so we know what to verify against - await store.startAttempt(runId, { attemptNumber: 1, isWarmStart: false }, { select: { id: true } }); + // Set status to EXECUTING first so we know what to verify against + await store.startAttempt( + runId, + { attemptNumber: 1, isWarmStart: false }, + { select: { id: true } } + ); - const run = await store.recordRetryOutcome( - runId, - { machinePreset: "large-1x", usageDurationMs: 200, costInCents: 5 }, - { include: { runtimeEnvironment: true } } - ); + const run = await store.recordRetryOutcome( + runId, + { machinePreset: "large-1x", usageDurationMs: 200, costInCents: 5 }, + { include: { runtimeEnvironment: true } } + ); - // Status must be unchanged (EXECUTING β€” not PENDING, not CANCELED) - expect(run.status).toBe("EXECUTING"); - expect(run.machinePreset).toBe("large-1x"); - expect(run.usageDurationMs).toBe(200); - expect(run.costInCents).toBe(5); - }); + // Status must be unchanged (EXECUTING β€” not PENDING, not CANCELED) + expect(run.status).toBe("EXECUTING"); + expect(run.machinePreset).toBe("large-1x"); + expect(run.usageDurationMs).toBe(200); + expect(run.costInCents).toBe(5); + } + ); postgresTest("requeueRun sets status to PENDING", async ({ prisma }) => { const { organization, project, environment } = await seedEnvironment(prisma); @@ -376,7 +397,11 @@ describe("PostgresRunStore", () => { }) ); - await store.startAttempt(runId, { attemptNumber: 1, isWarmStart: false }, { select: { id: true } }); + await store.startAttempt( + runId, + { attemptNumber: 1, isWarmStart: false }, + { select: { id: true } } + ); const run = await store.requeueRun(runId, { select: { id: true, status: true } }); @@ -384,48 +409,51 @@ describe("PostgresRunStore", () => { expect(run.status).toBe("PENDING"); }); - postgresTest("recordBulkActionMembership appends bulkActionId to existing array", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "recordBulkActionMembership appends bulkActionId to existing array", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_bulk_action_1"; + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_bulk_action_1"; - // Seed a run with an existing bulk action id - await prisma.taskRun.create({ - data: { - id: runId, - engine: "V2", - status: "CANCELED", - friendlyId: "run_bulk_action_friendly_1", - runtimeEnvironmentId: environment.id, - environmentType: "DEVELOPMENT", - organizationId: organization.id, - projectId: project.id, - taskIdentifier: "my-task", - payload: "{}", - payloadType: "application/json", - traceContext: {}, - traceId: "trace_b1", - spanId: "span_b1", - queue: "task/my-task", - isTest: false, - taskEventStore: "taskEvent", - depth: 0, - bulkActionGroupIds: ["existing-bulk-id"], - }, - }); + // Seed a run with an existing bulk action id + await prisma.taskRun.create({ + data: { + id: runId, + engine: "V2", + status: "CANCELED", + friendlyId: "run_bulk_action_friendly_1", + runtimeEnvironmentId: environment.id, + environmentType: "DEVELOPMENT", + organizationId: organization.id, + projectId: project.id, + taskIdentifier: "my-task", + payload: "{}", + payloadType: "application/json", + traceContext: {}, + traceId: "trace_b1", + spanId: "span_b1", + queue: "task/my-task", + isTest: false, + taskEventStore: "taskEvent", + depth: 0, + bulkActionGroupIds: ["existing-bulk-id"], + }, + }); - await store.recordBulkActionMembership(runId, "new-bulk-id"); + await store.recordBulkActionMembership(runId, "new-bulk-id"); - const updated = await prisma.taskRun.findUnique({ - where: { id: runId }, - select: { bulkActionGroupIds: true }, - }); + const updated = await prisma.taskRun.findUnique({ + where: { id: runId }, + select: { bulkActionGroupIds: true }, + }); - expect(updated?.bulkActionGroupIds).toContain("existing-bulk-id"); - expect(updated?.bulkActionGroupIds).toContain("new-bulk-id"); - expect(updated?.bulkActionGroupIds).toHaveLength(2); - }); + expect(updated?.bulkActionGroupIds).toContain("existing-bulk-id"); + expect(updated?.bulkActionGroupIds).toContain("new-bulk-id"); + expect(updated?.bulkActionGroupIds).toHaveLength(2); + } + ); postgresTest( "cancelRun sets status to CANCELED; without bulkActionId/usage those fields are untouched", @@ -466,7 +494,16 @@ describe("PostgresRunStore", () => { const run = await store.cancelRun( runId, { completedAt: cancelledAt, error }, - { select: { id: true, status: true, completedAt: true, bulkActionGroupIds: true, usageDurationMs: true, costInCents: true } } + { + select: { + id: true, + status: true, + completedAt: true, + bulkActionGroupIds: true, + usageDurationMs: true, + costInCents: true, + }, + } ); expect(run.id).toBe(runId); @@ -502,8 +539,22 @@ describe("PostgresRunStore", () => { const run = await store.cancelRun( runId, - { completedAt: cancelledAt, error, bulkActionId: "bulk-abc", usageDurationMs: 300, costInCents: 7 }, - { select: { id: true, status: true, bulkActionGroupIds: true, usageDurationMs: true, costInCents: true } } + { + completedAt: cancelledAt, + error, + bulkActionId: "bulk-abc", + usageDurationMs: 300, + costInCents: 7, + }, + { + select: { + id: true, + status: true, + bulkActionGroupIds: true, + usageDurationMs: true, + costInCents: true, + }, + } ); expect(run.status).toBe("CANCELED"); @@ -513,44 +564,47 @@ describe("PostgresRunStore", () => { } ); - postgresTest("failRunPermanently sets the passed status with completedAt/error/usage/cost", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "failRunPermanently sets the passed status with completedAt/error/usage/cost", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_fail_permanently_1"; + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_fail_permanently_1"; - await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - const completedAt = new Date("2026-05-01T00:00:00.000Z"); - const error = { type: "STRING_ERROR" as const, raw: "permanent failure" }; + const completedAt = new Date("2026-05-01T00:00:00.000Z"); + const error = { type: "STRING_ERROR" as const, raw: "permanent failure" }; - const run = await store.failRunPermanently( - runId, - { status: "SYSTEM_FAILURE", completedAt, error, usageDurationMs: 150, costInCents: 3 }, - { - select: { - id: true, - status: true, - completedAt: true, - usageDurationMs: true, - costInCents: true, - }, - } - ); + const run = await store.failRunPermanently( + runId, + { status: "SYSTEM_FAILURE", completedAt, error, usageDurationMs: 150, costInCents: 3 }, + { + select: { + id: true, + status: true, + completedAt: true, + usageDurationMs: true, + costInCents: true, + }, + } + ); - expect(run.id).toBe(runId); - expect(run.status).toBe("SYSTEM_FAILURE"); - expect(run.completedAt).toEqual(completedAt); - expect(run.usageDurationMs).toBe(150); - expect(run.costInCents).toBe(3); - }); + expect(run.id).toBe(runId); + expect(run.status).toBe("SYSTEM_FAILURE"); + expect(run.completedAt).toEqual(completedAt); + expect(run.usageDurationMs).toBe(150); + expect(run.costInCents).toBe(3); + } + ); postgresTest( "expireRun sets status to EXPIRED with distinct completedAt/expiredAt, error set, and one FINISHED/EXPIRED snapshot", @@ -571,7 +625,10 @@ describe("PostgresRunStore", () => { const completedAt = new Date("2026-06-01T10:00:00.000Z"); const expiredAt = new Date("2026-06-01T10:00:01.000Z"); - const error = { type: "STRING_ERROR" as const, raw: "Run expired because the TTL was reached" }; + const error = { + type: "STRING_ERROR" as const, + raw: "Run expired because the TTL was reached", + }; const run = await store.expireRun( runId, @@ -651,7 +708,10 @@ describe("PostgresRunStore", () => { } const now = new Date("2026-06-01T12:00:00.000Z"); - const error = { type: "STRING_ERROR" as const, raw: "Run expired because the TTL was reached" }; + const error = { + type: "STRING_ERROR" as const, + raw: "Run expired because the TTL was reached", + }; const count = await store.expireRunsBatch([runId1, runId2], { error, now }); @@ -827,31 +887,34 @@ describe("PostgresRunStore", () => { } ); - postgresTest("parkPendingVersion sets status to PENDING_VERSION and stores statusReason", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "parkPendingVersion sets status to PENDING_VERSION and stores statusReason", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_park_1"; + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_park_1"; - await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - const run = await store.parkPendingVersion( - runId, - { statusReason: "No background worker found" }, - { select: { id: true, status: true, statusReason: true } } - ); + const run = await store.parkPendingVersion( + runId, + { statusReason: "No background worker found" }, + { select: { id: true, status: true, statusReason: true } } + ); - expect(run.id).toBe(runId); - expect(run.status).toBe("PENDING_VERSION"); - expect(run.statusReason).toBe("No background worker found"); - }); + expect(run.id).toBe(runId); + expect(run.status).toBe("PENDING_VERSION"); + expect(run.statusReason).toBe("No background worker found"); + } + ); postgresTest( "promotePendingVersionRuns flips PENDING_VERSION to PENDING and returns count 1; run in another status returns count 0 and is unchanged", @@ -889,7 +952,10 @@ describe("PostgresRunStore", () => { expect(result.count).toBe(1); - const promoted = await prisma.taskRun.findUniqueOrThrow({ where: { id: pendingVersionId }, select: { status: true } }); + const promoted = await prisma.taskRun.findUniqueOrThrow({ + where: { id: pendingVersionId }, + select: { status: true }, + }); expect(promoted.status).toBe("PENDING"); // Seed a run NOT in PENDING_VERSION (e.g. EXECUTING) @@ -921,7 +987,10 @@ describe("PostgresRunStore", () => { expect(result2.count).toBe(0); - const unchanged = await prisma.taskRun.findUniqueOrThrow({ where: { id: executingId }, select: { status: true } }); + const unchanged = await prisma.taskRun.findUniqueOrThrow({ + where: { id: executingId }, + select: { status: true }, + }); expect(unchanged.status).toBe("EXECUTING"); } ); @@ -1585,7 +1654,9 @@ describe("PostgresRunStore β€” read", () => { const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - await expect(store.findRunOrThrow({ id: "missing" }, { select: { id: true } })).rejects.toThrow(); + await expect( + store.findRunOrThrow({ id: "missing" }, { select: { id: true } }) + ).rejects.toThrow(); }); postgresTest("findRun with include hydrates the relation", async ({ prisma }) => { @@ -1610,79 +1681,89 @@ describe("PostgresRunStore β€” read", () => { expect(run?.runtimeEnvironment.id).toBe(environment.id); }); - postgresTest("findRuns applies where/orderBy/take and returns ordered, limited rows", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "findRuns applies where/orderBy/take and returns ordered, limited rows", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const earliest = new Date("2026-06-01T00:00:00.000Z"); - const middle = new Date("2026-06-02T00:00:00.000Z"); - const latest = new Date("2026-06-03T00:00:00.000Z"); + const earliest = new Date("2026-06-01T00:00:00.000Z"); + const middle = new Date("2026-06-02T00:00:00.000Z"); + const latest = new Date("2026-06-03T00:00:00.000Z"); - const rows: Array<{ id: string; createdAt: Date }> = [ - { id: "run_find_many_earliest", createdAt: earliest }, - { id: "run_find_many_middle", createdAt: middle }, - { id: "run_find_many_latest", createdAt: latest }, - ]; + const rows: Array<{ id: string; createdAt: Date }> = [ + { id: "run_find_many_earliest", createdAt: earliest }, + { id: "run_find_many_middle", createdAt: middle }, + { id: "run_find_many_latest", createdAt: latest }, + ]; - for (const row of rows) { - await prisma.taskRun.create({ - data: { - id: row.id, - engine: "V2", - status: "PENDING", - friendlyId: `${row.id}_friendly`, - runtimeEnvironmentId: environment.id, - environmentType: "DEVELOPMENT", - organizationId: organization.id, - projectId: project.id, - taskIdentifier: "my-task", - payload: "{}", - payloadType: "application/json", - traceContext: {}, - traceId: `trace_${row.id}`, - spanId: `span_${row.id}`, - queue: "task/my-task", - isTest: false, - taskEventStore: "taskEvent", - depth: 0, - createdAt: row.createdAt, - }, - }); - } + for (const row of rows) { + await prisma.taskRun.create({ + data: { + id: row.id, + engine: "V2", + status: "PENDING", + friendlyId: `${row.id}_friendly`, + runtimeEnvironmentId: environment.id, + environmentType: "DEVELOPMENT", + organizationId: organization.id, + projectId: project.id, + taskIdentifier: "my-task", + payload: "{}", + payloadType: "application/json", + traceContext: {}, + traceId: `trace_${row.id}`, + spanId: `span_${row.id}`, + queue: "task/my-task", + isTest: false, + taskEventStore: "taskEvent", + depth: 0, + createdAt: row.createdAt, + }, + }); + } - const found = await store.findRuns({ - where: { projectId: project.id }, - select: { id: true }, - orderBy: { createdAt: "desc" }, - take: 2, - }); + const found = await store.findRuns({ + where: { projectId: project.id }, + select: { id: true }, + orderBy: { createdAt: "desc" }, + take: 2, + }); - expect(found).toEqual([{ id: "run_find_many_latest" }, { id: "run_find_many_middle" }]); - }); + expect(found).toEqual([{ id: "run_find_many_latest" }, { id: "run_find_many_middle" }]); + } + ); - postgresTest("findRun reads a just-written row when passed the writer client", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "findRun reads a just-written row when passed the writer client", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - // Use a NoopRunStore-style read replica that must NOT be hit: pass the writer - // (prisma) explicitly so reads go through it for read-after-write consistency. - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_find_read_after_write_1"; + // Use a NoopRunStore-style read replica that must NOT be hit: pass the writer + // (prisma) explicitly so reads go through it for read-after-write consistency. + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_find_read_after_write_1"; - await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - const run = await store.findRun({ id: runId }, { select: { id: true, status: true } }, prisma); + const run = await store.findRun( + { id: runId }, + { select: { id: true, status: true } }, + prisma + ); - expect(run?.id).toBe(runId); - expect(run?.status).toBe("PENDING"); - }); + expect(run?.id).toBe(runId); + expect(run?.status).toBe("PENDING"); + } + ); postgresTest("findRun by id with no projection returns the whole row", async ({ prisma }) => { const { organization, project, environment } = await seedEnvironment(prisma); @@ -1710,13 +1791,16 @@ describe("PostgresRunStore β€” read", () => { expect(run?.payloadType).toBe("application/json"); }); - postgresTest("findRunOrThrow with no projection throws when no row matches", async ({ prisma }) => { - await seedEnvironment(prisma); + postgresTest( + "findRunOrThrow with no projection throws when no row matches", + async ({ prisma }) => { + await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - await expect(store.findRunOrThrow({ id: "missing" })).rejects.toThrow(); - }); + await expect(store.findRunOrThrow({ id: "missing" })).rejects.toThrow(); + } + ); postgresTest("findRuns with no projection returns whole rows", async ({ prisma }) => { const { organization, project, environment } = await seedEnvironment(prisma); diff --git a/internal-packages/run-store/src/PostgresRunStore.ts b/internal-packages/run-store/src/PostgresRunStore.ts index 2caa5ca85b..79c85099d5 100644 --- a/internal-packages/run-store/src/PostgresRunStore.ts +++ b/internal-packages/run-store/src/PostgresRunStore.ts @@ -299,7 +299,12 @@ export class PostgresRunStore implements RunStore { async expireRun( runId: string, - data: { error: TaskRunError; completedAt: Date; expiredAt: Date; snapshot: ExpireSnapshotInput }, + data: { + error: TaskRunError; + completedAt: Date; + expiredAt: Date; + snapshot: ExpireSnapshotInput; + }, args: { select: S }, tx?: PrismaClientOrTransaction ): Promise> { @@ -629,10 +634,7 @@ export class PostgresRunStore implements RunStore { args: { include: I }, client?: ReadClient ): Promise | null>; - findRun( - where: Prisma.TaskRunWhereInput, - client?: ReadClient - ): Promise; + findRun(where: Prisma.TaskRunWhereInput, client?: ReadClient): Promise; async findRun( where: Prisma.TaskRunWhereInput, argsOrClient?: { select?: Prisma.TaskRunSelect; include?: Prisma.TaskRunInclude } | ReadClient, @@ -656,10 +658,7 @@ export class PostgresRunStore implements RunStore { args: { include: I }, client?: ReadClient ): Promise>; - findRunOrThrow( - where: Prisma.TaskRunWhereInput, - client?: ReadClient - ): Promise; + findRunOrThrow(where: Prisma.TaskRunWhereInput, client?: ReadClient): Promise; async findRunOrThrow( where: Prisma.TaskRunWhereInput, argsOrClient?: { select?: Prisma.TaskRunSelect; include?: Prisma.TaskRunInclude } | ReadClient, diff --git a/internal-packages/run-store/src/types.ts b/internal-packages/run-store/src/types.ts index 319ef18781..98eb3bb9e1 100644 --- a/internal-packages/run-store/src/types.ts +++ b/internal-packages/run-store/src/types.ts @@ -159,7 +159,12 @@ export type CreateRunInput = { }; export type CreateCancelledRunInput = { - data: CreateRunData & { error: Prisma.InputJsonValue; completedAt: Date; updatedAt: Date; attemptNumber: 0 }; + data: CreateRunData & { + error: Prisma.InputJsonValue; + completedAt: Date; + updatedAt: Date; + attemptNumber: 0; + }; snapshot: CreateRunSnapshotInput; }; @@ -227,7 +232,11 @@ export type RewriteDebouncedRunData = { export type ClearIdempotencyKeyInput = | { byId: { runId: string; idempotencyKey: string }; byPredicate?: never; byFriendlyIds?: never } - | { byPredicate: { idempotencyKey: string; taskIdentifier: string; runtimeEnvironmentId: string }; byId?: never; byFriendlyIds?: never } + | { + byPredicate: { idempotencyKey: string; taskIdentifier: string; runtimeEnvironmentId: string }; + byId?: never; + byFriendlyIds?: never; + } | { byFriendlyIds: string[]; byId?: never; byPredicate?: never }; export type TaskRunWithWaitpoint = TaskRun & { associatedWaitpoint: Waitpoint | null }; @@ -235,8 +244,14 @@ export type TaskRunWithWaitpoint = TaskRun & { associatedWaitpoint: Waitpoint | export interface RunStore { // Create createRun(params: CreateRunInput, tx?: PrismaClientOrTransaction): Promise; - createCancelledRun(params: CreateCancelledRunInput, tx?: PrismaClientOrTransaction): Promise; - createFailedRun(params: CreateFailedRunInput, tx?: PrismaClientOrTransaction): Promise; + createCancelledRun( + params: CreateCancelledRunInput, + tx?: PrismaClientOrTransaction + ): Promise; + createFailedRun( + params: CreateFailedRunInput, + tx?: PrismaClientOrTransaction + ): Promise; // Attempt lifecycle startAttempt( @@ -247,7 +262,14 @@ export interface RunStore { ): Promise>; completeAttemptSuccess( runId: string, - data: { completedAt: Date; output?: string; outputType: string; usageDurationMs: number; costInCents: number; snapshot: CompletionSnapshotInput }, + data: { + completedAt: Date; + output?: string; + outputType: string; + usageDurationMs: number; + costInCents: number; + snapshot: CompletionSnapshotInput; + }, args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; @@ -262,16 +284,32 @@ export interface RunStore { args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; - recordBulkActionMembership(runId: string, bulkActionId: string, tx?: PrismaClientOrTransaction): Promise; + recordBulkActionMembership( + runId: string, + bulkActionId: string, + tx?: PrismaClientOrTransaction + ): Promise; cancelRun( runId: string, - data: { completedAt?: Date; error: TaskRunError; bulkActionId?: string; usageDurationMs?: number; costInCents?: number }, + data: { + completedAt?: Date; + error: TaskRunError; + bulkActionId?: string; + usageDurationMs?: number; + costInCents?: number; + }, args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; failRunPermanently( runId: string, - data: { status: TaskRunStatus; completedAt: Date; error: TaskRunError; usageDurationMs: number; costInCents: number }, + data: { + status: TaskRunStatus; + completedAt: Date; + error: TaskRunError; + usageDurationMs: number; + costInCents: number; + }, args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; @@ -279,11 +317,20 @@ export interface RunStore { // Expiry expireRun( runId: string, - data: { error: TaskRunError; completedAt: Date; expiredAt: Date; snapshot: ExpireSnapshotInput }, + data: { + error: TaskRunError; + completedAt: Date; + expiredAt: Date; + snapshot: ExpireSnapshotInput; + }, args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; - expireRunsBatch(runIds: string[], data: { error: TaskRunError; now: Date }, tx?: PrismaClientOrTransaction): Promise; + expireRunsBatch( + runIds: string[], + data: { error: TaskRunError; now: Date }, + tx?: PrismaClientOrTransaction + ): Promise; // Dequeue / version / checkpoint lockRunToWorker( @@ -297,7 +344,10 @@ export interface RunStore { args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; - promotePendingVersionRuns(runId: string, tx?: PrismaClientOrTransaction): Promise<{ count: number }>; + promotePendingVersionRuns( + runId: string, + tx?: PrismaClientOrTransaction + ): Promise<{ count: number }>; suspendForCheckpoint( runId: string, args: { include: I }, @@ -315,19 +365,44 @@ export interface RunStore { data: { delayUntil: Date; queueTimestamp?: Date; snapshot?: RescheduleSnapshotInput }, tx?: PrismaClientOrTransaction ): Promise; - enqueueDelayedRun(runId: string, data: { queuedAt: Date }, tx?: PrismaClientOrTransaction): Promise; - rewriteDebouncedRun(runId: string, data: RewriteDebouncedRunData, tx?: PrismaClientOrTransaction): Promise; + enqueueDelayedRun( + runId: string, + data: { queuedAt: Date }, + tx?: PrismaClientOrTransaction + ): Promise; + rewriteDebouncedRun( + runId: string, + data: RewriteDebouncedRunData, + tx?: PrismaClientOrTransaction + ): Promise; // Field touches updateMetadata( runId: string, - data: { metadata: string | null; metadataType?: string; metadataVersion: { increment: number }; updatedAt: Date }, + data: { + metadata: string | null; + metadataType?: string; + metadataVersion: { increment: number }; + updatedAt: Date; + }, options: { expectedMetadataVersion?: number }, tx?: PrismaClientOrTransaction ): Promise<{ count: number }>; - clearIdempotencyKey(params: ClearIdempotencyKeyInput, tx?: PrismaClientOrTransaction): Promise<{ count: number }>; - pushTags(runId: string, tags: string[], where: { runtimeEnvironmentId: string }, tx?: PrismaClientOrTransaction): Promise<{ updatedAt: Date }>; - pushRealtimeStream(runId: string, streamId: string, tx?: PrismaClientOrTransaction): Promise; + clearIdempotencyKey( + params: ClearIdempotencyKeyInput, + tx?: PrismaClientOrTransaction + ): Promise<{ count: number }>; + pushTags( + runId: string, + tags: string[], + where: { runtimeEnvironmentId: string }, + tx?: PrismaClientOrTransaction + ): Promise<{ updatedAt: Date }>; + pushRealtimeStream( + runId: string, + streamId: string, + tx?: PrismaClientOrTransaction + ): Promise; // Read findRun( diff --git a/internal-packages/schedule-engine/package.json b/internal-packages/schedule-engine/package.json index 86929a3934..52a27a11e2 100644 --- a/internal-packages/schedule-engine/package.json +++ b/internal-packages/schedule-engine/package.json @@ -36,4 +36,4 @@ "build": "pnpm run clean && tsc -p tsconfig.build.json", "dev": "tsc --watch -p tsconfig.build.json" } -} \ No newline at end of file +} diff --git a/internal-packages/schedule-engine/src/engine/index.ts b/internal-packages/schedule-engine/src/engine/index.ts index 2c78beccbc..0d82d2b5f1 100644 --- a/internal-packages/schedule-engine/src/engine/index.ts +++ b/internal-packages/schedule-engine/src/engine/index.ts @@ -576,7 +576,7 @@ export class ScheduleEngine { // last-fire timestamp with a series of skipped slots. const carriedLastScheduleTime = shouldTrigger ? scheduleTimestamp - : params.lastScheduleTime ?? instance.lastScheduledTimestamp ?? undefined; + : (params.lastScheduleTime ?? instance.lastScheduledTimestamp ?? undefined); const [nextRunError] = await tryCatch( this.registerNextTaskScheduleInstance({ diff --git a/internal-packages/schedule-engine/test/scheduleRecovery.test.ts b/internal-packages/schedule-engine/test/scheduleRecovery.test.ts index 40ce4b1bba..b791d93654 100644 --- a/internal-packages/schedule-engine/test/scheduleRecovery.test.ts +++ b/internal-packages/schedule-engine/test/scheduleRecovery.test.ts @@ -565,7 +565,8 @@ describe("Schedule Recovery", () => { const job = await engine.getJob(`scheduled-task-instance:${scheduleInstance.id}`); expect(job).not.toBeNull(); - const enqueuedLastScheduleTime = (job?.item as { lastScheduleTime?: Date }).lastScheduleTime; + const enqueuedLastScheduleTime = (job?.item as { lastScheduleTime?: Date }) + .lastScheduleTime; // Brand-new schedule: cron's previous slot predates instance.createdAt, // so the function leaves lastScheduleTime undefined β€” the first fire // will report `payload.lastTimestamp: undefined` and customer first-run diff --git a/internal-packages/sdk-compat-tests/src/fixtures/deno/test.ts b/internal-packages/sdk-compat-tests/src/fixtures/deno/test.ts index 6894606fd0..87283db3aa 100644 --- a/internal-packages/sdk-compat-tests/src/fixtures/deno/test.ts +++ b/internal-packages/sdk-compat-tests/src/fixtures/deno/test.ts @@ -6,7 +6,18 @@ */ // Use bare specifier - resolved via node_modules when nodeModulesDir is enabled -import { task, logger, schedules, runs, configure, queue, retry, wait, metadata, tags } from "@trigger.dev/sdk"; +import { + task, + logger, + schedules, + runs, + configure, + queue, + retry, + wait, + metadata, + tags, +} from "@trigger.dev/sdk"; // Validate exports exist const checks: [string, boolean][] = [ diff --git a/internal-packages/sdk-compat-tests/src/fixtures/esm-import/test.mjs b/internal-packages/sdk-compat-tests/src/fixtures/esm-import/test.mjs index 70b055aaa2..a90aaaa1d8 100644 --- a/internal-packages/sdk-compat-tests/src/fixtures/esm-import/test.mjs +++ b/internal-packages/sdk-compat-tests/src/fixtures/esm-import/test.mjs @@ -6,7 +6,18 @@ */ // Test main export -import { task, logger, schedules, runs, configure, queue, retry, wait, metadata, tags } from "@trigger.dev/sdk"; +import { + task, + logger, + schedules, + runs, + configure, + queue, + retry, + wait, + metadata, + tags, +} from "@trigger.dev/sdk"; // Test /v3 subpath (legacy, but should still work) import { task as taskV3 } from "@trigger.dev/sdk/v3"; diff --git a/internal-packages/sso/src/fallback.ts b/internal-packages/sso/src/fallback.ts index 564eb3391b..6d98e7c657 100644 --- a/internal-packages/sso/src/fallback.ts +++ b/internal-packages/sso/src/fallback.ts @@ -102,10 +102,7 @@ class SsoFallbackController implements SsoController { return errAsync("feature_disabled" as const); } - completeAuthorization(_params: { - code: string; - state: string; - }): ResultAsync< + completeAuthorization(_params: { code: string; state: string }): ResultAsync< { profile: SsoProfile; redirectTo: string; diff --git a/internal-packages/sso/src/index.ts b/internal-packages/sso/src/index.ts index aec22b81ce..5f65ae6f97 100644 --- a/internal-packages/sso/src/index.ts +++ b/internal-packages/sso/src/index.ts @@ -20,9 +20,7 @@ import { ResultAsync } from "neverthrow"; import { SsoFallback } from "./fallback.js"; export type { SsoController } from "@trigger.dev/plugins"; -export type SsoPrismaInput = - | PrismaClient - | { primary: PrismaClient; replica: PrismaClient }; +export type SsoPrismaInput = PrismaClient | { primary: PrismaClient; replica: PrismaClient }; export type SsoCreateOptions = { // When true, skip loading the plugin. Useful for tests and for @@ -44,17 +42,13 @@ export class LazyController implements SsoController { this._init = this.load(prisma, options); } - private async load( - prisma: SsoPrismaInput, - options?: SsoCreateOptions - ): Promise { + private async load(prisma: SsoPrismaInput, options?: SsoCreateOptions): Promise { if (options?.forceFallback) { return new SsoFallback(prisma).create(); } const moduleName = "@triggerdotdev/plugins/sso"; const importer = - options?.importer ?? - ((m: string) => import(m) as Promise<{ default: SsoPlugin }>); + options?.importer ?? ((m: string) => import(m) as Promise<{ default: SsoPlugin }>); try { const module = await importer(moduleName); const plugin: SsoPlugin = module.default; @@ -75,10 +69,8 @@ export class LazyController implements SsoController { // plugin's own module name. const code = (err as NodeJS.ErrnoException | undefined)?.code; const message = err instanceof Error ? err.message : String(err); - const isModuleNotFound = - code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; - const isPluginItselfMissing = - isModuleNotFound && message.includes(moduleName); + const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; + const isPluginItselfMissing = isModuleNotFound && message.includes(moduleName); if (!isPluginItselfMissing) { console.error( diff --git a/internal-packages/sso/src/loader.test.ts b/internal-packages/sso/src/loader.test.ts index fa05f2947e..de2c6731e1 100644 --- a/internal-packages/sso/src/loader.test.ts +++ b/internal-packages/sso/src/loader.test.ts @@ -122,10 +122,7 @@ describe("SSO LazyController", () => { const controller = new LazyController(fakePrisma, { importer }); await controller.isUsingPlugin(); const fallbackLogs = logSpy.mock.calls.filter((args) => - args.some( - (a) => - typeof a === "string" && a.includes("no plugin installed") - ) + args.some((a) => typeof a === "string" && a.includes("no plugin installed")) ); expect(fallbackLogs.length).toBe(0); expect(errorSpy).not.toHaveBeenCalled(); @@ -152,9 +149,7 @@ describe("SSO LazyController", () => { const controller = new LazyController(fakePrisma, { importer }); await controller.isUsingPlugin(); const fallbackLogs = logSpy.mock.calls.filter((args) => - args.some( - (a) => typeof a === "string" && a.includes("no plugin installed") - ) + args.some((a) => typeof a === "string" && a.includes("no plugin installed")) ); expect(fallbackLogs.length).toBe(1); } finally { @@ -170,10 +165,9 @@ describe("SSO LazyController", () => { const importer = vi.fn(async () => { // Module-not-found from a *transitive* dep, not the plugin // itself β€” its `message` won't contain the plugin's moduleName. - const err = Object.assign( - new Error(`Cannot find module 'some-transitive-dep'`), - { code: "ERR_MODULE_NOT_FOUND" } - ); + const err = Object.assign(new Error(`Cannot find module 'some-transitive-dep'`), { + code: "ERR_MODULE_NOT_FOUND", + }); throw err; }); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); diff --git a/internal-packages/testcontainers/src/webapp.ts b/internal-packages/testcontainers/src/webapp.ts index 0c24ec546a..02f36dd9c4 100644 --- a/internal-packages/testcontainers/src/webapp.ts +++ b/internal-packages/testcontainers/src/webapp.ts @@ -112,14 +112,14 @@ export async function startWebapp( REDIS_TLS_DISABLED: "true", // all *_REDIS_TLS_DISABLED vars default to this; test Redis has no TLS // Disable all background workers. Each worker has its own env var and its own // check idiom ("0" vs "false" vs boolean), so we set all of them explicitly. - WORKER_ENABLED: "false", // disables workerQueue.initialize() (checked === "true") - RUN_ENGINE_WORKER_ENABLED: "0", // disables run engine workers (checked === "0", default "1") - SCHEDULE_WORKER_ENABLED: "0", // disables schedule engine worker (checked === "0") + WORKER_ENABLED: "false", // disables workerQueue.initialize() (checked === "true") + RUN_ENGINE_WORKER_ENABLED: "0", // disables run engine workers (checked === "0", default "1") + SCHEDULE_WORKER_ENABLED: "0", // disables schedule engine worker (checked === "0") BATCH_QUEUE_WORKER_ENABLED: "false", // disables batch queue consumers (BoolEnv) LEGACY_RUN_ENGINE_WORKER_ENABLED: "0", // disables legacy run engine worker - COMMON_WORKER_ENABLED: "0", // disables common worker - RUN_ENGINE_TTL_SYSTEM_DISABLED: "true", // disables TTL expiry system (BoolEnv) - RUN_ENGINE_TTL_CONSUMERS_DISABLED: "true", // disables TTL consumers (BoolEnv) + COMMON_WORKER_ENABLED: "0", // disables common worker + RUN_ENGINE_TTL_SYSTEM_DISABLED: "true", // disables TTL expiry system (BoolEnv) + RUN_ENGINE_TTL_CONSUMERS_DISABLED: "true", // disables TTL consumers (BoolEnv) RUN_REPLICATION_ENABLED: "0", // Force the RBAC loader to use the default fallback in e2e tests // so auth behaviour is deterministic regardless of whether a @@ -208,9 +208,7 @@ export interface TestServer { } /** Convenience helper: starts a postgres + redis container + webapp and returns both for testing. */ -export async function startTestServer( - options: StartWebappOptions = {} -): Promise { +export async function startTestServer(options: StartWebappOptions = {}): Promise { const network = await new Network().start(); // Track each resource as we acquire it so we can tear it down if a later step fails. @@ -231,11 +229,7 @@ export async function startTestServer( prisma = new PrismaClient({ datasources: { db: { url: pg.url } } }); await prisma.$connect(); // pre-warm pool; surface connection failures before tests start - const started = await startWebapp( - pg.url, - { host: rc.getHost(), port: rc.getPort() }, - options - ); + const started = await startWebapp(pg.url, { host: rc.getHost(), port: rc.getPort() }, options); webapp = started.instance; stopWebapp = started.stop; } catch (err) { diff --git a/internal-packages/tracing/package.json b/internal-packages/tracing/package.json index 7474ec249e..a3aa10bb15 100644 --- a/internal-packages/tracing/package.json +++ b/internal-packages/tracing/package.json @@ -14,4 +14,4 @@ "scripts": { "typecheck": "tsc --noEmit" } -} \ No newline at end of file +} diff --git a/internal-packages/tsql/src/index.test.ts b/internal-packages/tsql/src/index.test.ts index a93fb0fd4b..3c958635e8 100644 --- a/internal-packages/tsql/src/index.test.ts +++ b/internal-packages/tsql/src/index.test.ts @@ -113,9 +113,7 @@ describe("isColumnReferencedInExpression", () => { }); it("should detect column in qualified reference (table.column)", () => { - const ast = parseTSQLSelect( - "SELECT * FROM task_runs WHERE task_runs.time > '2024-01-01'" - ); + const ast = parseTSQLSelect("SELECT * FROM task_runs WHERE task_runs.time > '2024-01-01'"); if (ast.expression_type === "select_query") { expect(isColumnReferencedInExpression(ast.where, "time")).toBe(true); } @@ -408,15 +406,12 @@ describe("compileTSQL with whereClauseFallback", () => { }); it("should NOT apply fallback when column is in qualified reference", () => { - const { sql } = compileTSQL( - "SELECT * FROM task_runs WHERE task_runs.time > '2024-06-01'", - { - ...baseOptions, - whereClauseFallback: { - time: { op: "gte", value: "2024-01-01" }, - }, - } - ); + const { sql } = compileTSQL("SELECT * FROM task_runs WHERE task_runs.time > '2024-06-01'", { + ...baseOptions, + whereClauseFallback: { + time: { op: "gte", value: "2024-01-01" }, + }, + }); // Fallback should not be applied const timeGreaterMatches = sql.match(/greater.*time/g) || []; @@ -588,16 +583,13 @@ describe("compileTSQL with enforcedWhereClause", () => { }); it("should apply enforced condition even when user filters on same field", () => { - const { sql } = compileTSQL( - "SELECT id FROM task_runs WHERE triggered_at > '2025-01-01'", - { - tableSchema: [taskRunsSchema], - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_123" }, - triggered_at: { op: "gte", value: "2024-01-01" }, - }, - } - ); + const { sql } = compileTSQL("SELECT id FROM task_runs WHERE triggered_at > '2025-01-01'", { + tableSchema: [taskRunsSchema], + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_123" }, + triggered_at: { op: "gte", value: "2024-01-01" }, + }, + }); // Should have BOTH the user's condition AND the enforced condition // User's condition: greater(triggered_at, '2025-01-01') @@ -681,19 +673,16 @@ describe("compileTSQL with enforcedWhereClause", () => { }); it("should apply enforced but not fallback when user filters on fallback column", () => { - const { sql, params } = compileTSQL( - "SELECT id FROM task_runs WHERE status = 'failed'", - { - tableSchema: [taskRunsSchema], - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_123" }, - triggered_at: { op: "gte", value: "2024-01-01" }, - }, - whereClauseFallback: { - status: { op: "eq", value: "completed" }, - }, - } - ); + const { sql, params } = compileTSQL("SELECT id FROM task_runs WHERE status = 'failed'", { + tableSchema: [taskRunsSchema], + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_123" }, + triggered_at: { op: "gte", value: "2024-01-01" }, + }, + whereClauseFallback: { + status: { op: "eq", value: "completed" }, + }, + }); // Enforced triggered_at should be applied expect(sql).toContain("triggered_at"); @@ -722,19 +711,16 @@ describe("compileTSQL with enforcedWhereClause", () => { }); it("should skip fallback but keep enforced when user filters on same field", () => { - const { sql } = compileTSQL( - "SELECT id FROM task_runs WHERE triggered_at > '2025-01-01'", - { - tableSchema: [taskRunsSchema], - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_123" }, - triggered_at: { op: "gte", value: "2024-06-01" }, // Enforced: always applied - }, - whereClauseFallback: { - triggered_at: { op: "gte", value: "2024-01-01" }, // Fallback: skipped since user filtered - }, - } - ); + const { sql } = compileTSQL("SELECT id FROM task_runs WHERE triggered_at > '2025-01-01'", { + tableSchema: [taskRunsSchema], + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_123" }, + triggered_at: { op: "gte", value: "2024-06-01" }, // Enforced: always applied + }, + whereClauseFallback: { + triggered_at: { op: "gte", value: "2024-01-01" }, // Fallback: skipped since user filtered + }, + }); // User's condition + enforced should be present // Fallback should NOT be applied since user filtered on triggered_at @@ -767,16 +753,13 @@ describe("compileTSQL with enforcedWhereClause", () => { }); it("should NOT be bypassable via OR clause", () => { - const { sql } = compileTSQL( - "SELECT id FROM task_runs WHERE status = 'completed' OR 1=1", - { - tableSchema: [taskRunsSchema], - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_123" }, - triggered_at: { op: "gte", value: "2024-01-01" }, - }, - } - ); + const { sql } = compileTSQL("SELECT id FROM task_runs WHERE status = 'completed' OR 1=1", { + tableSchema: [taskRunsSchema], + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_123" }, + triggered_at: { op: "gte", value: "2024-01-01" }, + }, + }); // The enforced conditions should be ANDed with the entire user WHERE clause // So the structure should be: (enforced AND enforced AND ...) AND (user_where) @@ -825,4 +808,3 @@ describe("compileTSQL with enforcedWhereClause", () => { }); }); }); - diff --git a/internal-packages/tsql/src/query/database.ts b/internal-packages/tsql/src/query/database.ts index e91e0132c5..7961ed4d6b 100644 --- a/internal-packages/tsql/src/query/database.ts +++ b/internal-packages/tsql/src/query/database.ts @@ -452,8 +452,8 @@ function constantTypeToSerializedFieldType( return printed === "String" ? DatabaseSerializedFieldType.STRING : printed === "JSON" - ? DatabaseSerializedFieldType.JSON - : DatabaseSerializedFieldType.ARRAY; + ? DatabaseSerializedFieldType.JSON + : DatabaseSerializedFieldType.ARRAY; } if (printed === "Boolean") return DatabaseSerializedFieldType.BOOLEAN; if (printed === "Date") return DatabaseSerializedFieldType.DATE; diff --git a/internal-packages/tsql/src/query/functions.ts b/internal-packages/tsql/src/query/functions.ts index f184ed8f38..2f2b927845 100644 --- a/internal-packages/tsql/src/query/functions.ts +++ b/internal-packages/tsql/src/query/functions.ts @@ -136,7 +136,11 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { substr: { clickhouseName: "substring", minArgs: 2, maxArgs: 3 }, mid: { clickhouseName: "substring", minArgs: 2, maxArgs: 3 }, substringUTF8: { clickhouseName: "substringUTF8", minArgs: 2, maxArgs: 3 }, - appendTrailingCharIfAbsent: { clickhouseName: "appendTrailingCharIfAbsent", minArgs: 2, maxArgs: 2 }, + appendTrailingCharIfAbsent: { + clickhouseName: "appendTrailingCharIfAbsent", + minArgs: 2, + maxArgs: 2, + }, convertCharset: { clickhouseName: "convertCharset", minArgs: 3, maxArgs: 3 }, base58Encode: { clickhouseName: "base58Encode", minArgs: 1, maxArgs: 1 }, base58Decode: { clickhouseName: "base58Decode", minArgs: 1, maxArgs: 1 }, @@ -166,7 +170,11 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { position: { clickhouseName: "position", minArgs: 2, maxArgs: 2 }, positionCaseInsensitive: { clickhouseName: "positionCaseInsensitive", minArgs: 2, maxArgs: 2 }, positionUTF8: { clickhouseName: "positionUTF8", minArgs: 2, maxArgs: 2 }, - positionCaseInsensitiveUTF8: { clickhouseName: "positionCaseInsensitiveUTF8", minArgs: 2, maxArgs: 2 }, + positionCaseInsensitiveUTF8: { + clickhouseName: "positionCaseInsensitiveUTF8", + minArgs: 2, + maxArgs: 2, + }, locate: { clickhouseName: "locate", minArgs: 2, maxArgs: 2 }, match: { clickhouseName: "match", minArgs: 2, maxArgs: 2 }, multiMatchAny: { clickhouseName: "multiMatchAny", minArgs: 2, maxArgs: 2 }, @@ -177,7 +185,11 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { multiSearchAny: { clickhouseName: "multiSearchAny", minArgs: 2, maxArgs: 2 }, extract: { clickhouseName: "extract", minArgs: 2, maxArgs: 2 }, extractAll: { clickhouseName: "extractAll", minArgs: 2, maxArgs: 2 }, - extractAllGroupsHorizontal: { clickhouseName: "extractAllGroupsHorizontal", minArgs: 2, maxArgs: 2 }, + extractAllGroupsHorizontal: { + clickhouseName: "extractAllGroupsHorizontal", + minArgs: 2, + maxArgs: 2, + }, extractAllGroupsVertical: { clickhouseName: "extractAllGroupsVertical", minArgs: 2, maxArgs: 2 }, like: { clickhouseName: "like", minArgs: 2, maxArgs: 2 }, ilike: { clickhouseName: "ilike", minArgs: 2, maxArgs: 2 }, @@ -294,12 +306,42 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { toTimeZone: { clickhouseName: "toTimeZone", minArgs: 2, maxArgs: 2 }, formatDateTime: { clickhouseName: "formatDateTime", minArgs: 2, maxArgs: 3 }, parseDateTime: { clickhouseName: "parseDateTime", minArgs: 2, maxArgs: 3 }, - parseDateTimeBestEffort: { clickhouseName: "parseDateTimeBestEffort", minArgs: 1, maxArgs: 2, tzAware: true }, - parseDateTimeBestEffortOrNull: { clickhouseName: "parseDateTimeBestEffortOrNull", minArgs: 1, maxArgs: 2, tzAware: true }, - parseDateTimeBestEffortOrZero: { clickhouseName: "parseDateTimeBestEffortOrZero", minArgs: 1, maxArgs: 2, tzAware: true }, - parseDateTime64BestEffort: { clickhouseName: "parseDateTime64BestEffort", minArgs: 1, maxArgs: 3, tzAware: true }, - parseDateTime64BestEffortOrNull: { clickhouseName: "parseDateTime64BestEffortOrNull", minArgs: 1, maxArgs: 3, tzAware: true }, - parseDateTime64BestEffortOrZero: { clickhouseName: "parseDateTime64BestEffortOrZero", minArgs: 1, maxArgs: 3, tzAware: true }, + parseDateTimeBestEffort: { + clickhouseName: "parseDateTimeBestEffort", + minArgs: 1, + maxArgs: 2, + tzAware: true, + }, + parseDateTimeBestEffortOrNull: { + clickhouseName: "parseDateTimeBestEffortOrNull", + minArgs: 1, + maxArgs: 2, + tzAware: true, + }, + parseDateTimeBestEffortOrZero: { + clickhouseName: "parseDateTimeBestEffortOrZero", + minArgs: 1, + maxArgs: 2, + tzAware: true, + }, + parseDateTime64BestEffort: { + clickhouseName: "parseDateTime64BestEffort", + minArgs: 1, + maxArgs: 3, + tzAware: true, + }, + parseDateTime64BestEffortOrNull: { + clickhouseName: "parseDateTime64BestEffortOrNull", + minArgs: 1, + maxArgs: 3, + tzAware: true, + }, + parseDateTime64BestEffortOrZero: { + clickhouseName: "parseDateTime64BestEffortOrZero", + minArgs: 1, + maxArgs: 3, + tzAware: true, + }, // Interval functions toIntervalSecond: { clickhouseName: "toIntervalSecond", minArgs: 1, maxArgs: 1 }, @@ -413,9 +455,21 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { domain: { clickhouseName: "domain", minArgs: 1, maxArgs: 1 }, domainWithoutWWW: { clickhouseName: "domainWithoutWWW", minArgs: 1, maxArgs: 1 }, topLevelDomain: { clickhouseName: "topLevelDomain", minArgs: 1, maxArgs: 1 }, - firstSignificantSubdomain: { clickhouseName: "firstSignificantSubdomain", minArgs: 1, maxArgs: 1 }, - cutToFirstSignificantSubdomain: { clickhouseName: "cutToFirstSignificantSubdomain", minArgs: 1, maxArgs: 1 }, - cutToFirstSignificantSubdomainWithWWW: { clickhouseName: "cutToFirstSignificantSubdomainWithWWW", minArgs: 1, maxArgs: 1 }, + firstSignificantSubdomain: { + clickhouseName: "firstSignificantSubdomain", + minArgs: 1, + maxArgs: 1, + }, + cutToFirstSignificantSubdomain: { + clickhouseName: "cutToFirstSignificantSubdomain", + minArgs: 1, + maxArgs: 1, + }, + cutToFirstSignificantSubdomainWithWWW: { + clickhouseName: "cutToFirstSignificantSubdomainWithWWW", + minArgs: 1, + maxArgs: 1, + }, port: { clickhouseName: "port", minArgs: 1, maxArgs: 2 }, path: { clickhouseName: "path", minArgs: 1, maxArgs: 1 }, pathFull: { clickhouseName: "pathFull", minArgs: 1, maxArgs: 1 }, @@ -438,7 +492,11 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { isNaN: { clickhouseName: "isNaN", minArgs: 1, maxArgs: 1 }, bar: { clickhouseName: "bar", minArgs: 4, maxArgs: 4 }, transform: { clickhouseName: "transform", minArgs: 3, maxArgs: 4 }, - formatReadableDecimalSize: { clickhouseName: "formatReadableDecimalSize", minArgs: 1, maxArgs: 1 }, + formatReadableDecimalSize: { + clickhouseName: "formatReadableDecimalSize", + minArgs: 1, + maxArgs: 1, + }, formatReadableSize: { clickhouseName: "formatReadableSize", minArgs: 1, maxArgs: 1 }, formatReadableQuantity: { clickhouseName: "formatReadableQuantity", minArgs: 1, maxArgs: 1 }, formatReadableTimeDelta: { clickhouseName: "formatReadableTimeDelta", minArgs: 1, maxArgs: 2 }, @@ -447,7 +505,11 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { min2: { clickhouseName: "min2", minArgs: 2, maxArgs: 2 }, max2: { clickhouseName: "max2", minArgs: 2, maxArgs: 2 }, runningDifference: { clickhouseName: "runningDifference", minArgs: 1, maxArgs: 1 }, - runningDifferenceStartingWithFirstValue: { clickhouseName: "runningDifferenceStartingWithFirstValue", minArgs: 1, maxArgs: 1 }, + runningDifferenceStartingWithFirstValue: { + clickhouseName: "runningDifferenceStartingWithFirstValue", + minArgs: 1, + maxArgs: 1, + }, neighbor: { clickhouseName: "neighbor", minArgs: 2, maxArgs: 3 }, // Window functions @@ -504,10 +566,32 @@ export const TSQL_AGGREGATIONS: Record = { groupArrayIf: { clickhouseName: "groupArrayIf", minArgs: 2, maxArgs: 2, aggregate: true }, groupUniqArray: { clickhouseName: "groupUniqArray", minArgs: 1, maxArgs: 1, aggregate: true }, groupUniqArrayIf: { clickhouseName: "groupUniqArrayIf", minArgs: 2, maxArgs: 2, aggregate: true }, - groupArrayInsertAt: { clickhouseName: "groupArrayInsertAt", minArgs: 2, maxArgs: 2, aggregate: true }, - groupArrayMovingAvg: { clickhouseName: "groupArrayMovingAvg", minArgs: 1, maxArgs: 1, aggregate: true }, - groupArrayMovingSum: { clickhouseName: "groupArrayMovingSum", minArgs: 1, maxArgs: 1, aggregate: true }, - groupArraySample: { clickhouseName: "groupArraySample", minArgs: 1, maxArgs: 1, minParams: 1, maxParams: 2, aggregate: true }, + groupArrayInsertAt: { + clickhouseName: "groupArrayInsertAt", + minArgs: 2, + maxArgs: 2, + aggregate: true, + }, + groupArrayMovingAvg: { + clickhouseName: "groupArrayMovingAvg", + minArgs: 1, + maxArgs: 1, + aggregate: true, + }, + groupArrayMovingSum: { + clickhouseName: "groupArrayMovingSum", + minArgs: 1, + maxArgs: 1, + aggregate: true, + }, + groupArraySample: { + clickhouseName: "groupArraySample", + minArgs: 1, + maxArgs: 1, + minParams: 1, + maxParams: 2, + aggregate: true, + }, array_agg: { clickhouseName: "groupArray", minArgs: 1, maxArgs: 1, aggregate: true }, // Bitmap aggregations @@ -528,12 +612,39 @@ export const TSQL_AGGREGATIONS: Record = { median: { clickhouseName: "median", minArgs: 1, maxArgs: 1, aggregate: true }, medianIf: { clickhouseName: "medianIf", minArgs: 2, maxArgs: 2, aggregate: true }, medianExact: { clickhouseName: "medianExact", minArgs: 1, maxArgs: 1, aggregate: true }, - quantile: { clickhouseName: "quantile", minArgs: 1, maxArgs: 1, minParams: 1, maxParams: 1, aggregate: true }, - quantileIf: { clickhouseName: "quantileIf", minArgs: 2, maxArgs: 2, minParams: 1, maxParams: 1, aggregate: true }, + quantile: { + clickhouseName: "quantile", + minArgs: 1, + maxArgs: 1, + minParams: 1, + maxParams: 1, + aggregate: true, + }, + quantileIf: { + clickhouseName: "quantileIf", + minArgs: 2, + maxArgs: 2, + minParams: 1, + maxParams: 1, + aggregate: true, + }, quantiles: { clickhouseName: "quantiles", minArgs: 1, aggregate: true }, // -Merge combinators for AggregatingMergeTree tables - quantilesMerge: { clickhouseName: "quantilesMerge", minArgs: 1, maxArgs: 1, minParams: 1, aggregate: true }, - quantileMerge: { clickhouseName: "quantileMerge", minArgs: 1, maxArgs: 1, minParams: 1, maxParams: 1, aggregate: true }, + quantilesMerge: { + clickhouseName: "quantilesMerge", + minArgs: 1, + maxArgs: 1, + minParams: 1, + aggregate: true, + }, + quantileMerge: { + clickhouseName: "quantileMerge", + minArgs: 1, + maxArgs: 1, + minParams: 1, + maxParams: 1, + aggregate: true, + }, sumMerge: { clickhouseName: "sumMerge", minArgs: 1, maxArgs: 1, aggregate: true }, avgMerge: { clickhouseName: "avgMerge", minArgs: 1, maxArgs: 1, aggregate: true }, countMerge: { clickhouseName: "countMerge", minArgs: 1, maxArgs: 1, aggregate: true }, @@ -541,7 +652,12 @@ export const TSQL_AGGREGATIONS: Record = { maxMerge: { clickhouseName: "maxMerge", minArgs: 1, maxArgs: 1, aggregate: true }, // Statistical functions - simpleLinearRegression: { clickhouseName: "simpleLinearRegression", minArgs: 2, maxArgs: 2, aggregate: true }, + simpleLinearRegression: { + clickhouseName: "simpleLinearRegression", + minArgs: 2, + maxArgs: 2, + aggregate: true, + }, contingency: { clickhouseName: "contingency", minArgs: 2, maxArgs: 2, aggregate: true }, cramersV: { clickhouseName: "cramersV", minArgs: 2, maxArgs: 2, aggregate: true }, theilsU: { clickhouseName: "theilsU", minArgs: 2, maxArgs: 2, aggregate: true }, @@ -552,7 +668,14 @@ export const TSQL_AGGREGATIONS: Record = { maxMap: { clickhouseName: "maxMap", minArgs: 1, maxArgs: 2, aggregate: true }, // TopK - topK: { clickhouseName: "topK", minArgs: 1, maxArgs: 1, minParams: 1, maxParams: 1, aggregate: true }, + topK: { + clickhouseName: "topK", + minArgs: 1, + maxArgs: 1, + minParams: 1, + maxParams: 1, + aggregate: true, + }, // Funnel windowFunnel: { clickhouseName: "windowFunnel", minArgs: 1, maxArgs: 99, aggregate: true }, @@ -623,7 +746,9 @@ export function findTSQLFunction(name: string): TSQLFunctionMeta | undefined { * Get all exposed function names (for autocomplete, suggestions, etc.) */ export function getAllExposedFunctionNames(): string[] { - const functionNames = Object.keys(TSQL_CLICKHOUSE_FUNCTIONS).filter((name) => !name.startsWith("_")); + const functionNames = Object.keys(TSQL_CLICKHOUSE_FUNCTIONS).filter( + (name) => !name.startsWith("_") + ); const aggregationNames = Object.keys(TSQL_AGGREGATIONS).filter((name) => !name.startsWith("_")); return [...functionNames, ...aggregationNames]; } @@ -662,4 +787,3 @@ export function validateFunctionArgs( ); } } - diff --git a/internal-packages/tsql/src/query/parse_string.ts b/internal-packages/tsql/src/query/parse_string.ts index be38f752dd..996c4e5494 100644 --- a/internal-packages/tsql/src/query/parse_string.ts +++ b/internal-packages/tsql/src/query/parse_string.ts @@ -1,65 +1,69 @@ // TypeScript translation of posthog/hogql/parse_string.py // Keep this file in sync with the Python version -import { SyntaxError } from './errors'; +import { SyntaxError } from "./errors"; function replaceCommonEscapeCharacters(text: string): string { - // copied from clickhouse_driver/util/escape.py - // Note: \a (bell) and \v (vertical tab) are not directly supported in JavaScript strings - // but we handle them as escape sequences that get replaced - text = text.replace(/\\b/g, '\b'); - text = text.replace(/\\f/g, '\f'); - text = text.replace(/\\r/g, '\r'); - text = text.replace(/\\n/g, '\n'); - text = text.replace(/\\t/g, '\t'); - text = text.replace(/\\0/g, ''); // NUL characters are ignored - text = text.replace(/\\a/g, '\x07'); // Bell character (ASCII 7) - text = text.replace(/\\v/g, '\x0B'); // Vertical tab (ASCII 11) - text = text.replace(/\\\\/g, '\\'); - return text; + // copied from clickhouse_driver/util/escape.py + // Note: \a (bell) and \v (vertical tab) are not directly supported in JavaScript strings + // but we handle them as escape sequences that get replaced + text = text.replace(/\\b/g, "\b"); + text = text.replace(/\\f/g, "\f"); + text = text.replace(/\\r/g, "\r"); + text = text.replace(/\\n/g, "\n"); + text = text.replace(/\\t/g, "\t"); + text = text.replace(/\\0/g, ""); // NUL characters are ignored + text = text.replace(/\\a/g, "\x07"); // Bell character (ASCII 7) + text = text.replace(/\\v/g, "\x0B"); // Vertical tab (ASCII 11) + text = text.replace(/\\\\/g, "\\"); + return text; } export function parseStringLiteralText(text: string): string { - /** Converts a string received from antlr via ctx.getText() into a JavaScript string */ - let result: string; - - if (text.startsWith("'") && text.endsWith("'")) { - result = text.slice(1, -1); - result = result.replace(/''/g, "'"); - result = result.replace(/\\'/g, "'"); - } else if (text.startsWith('"') && text.endsWith('"')) { - result = text.slice(1, -1); - result = result.replace(/""/g, '"'); - result = result.replace(/\\"/g, '"'); - } else if (text.startsWith('`') && text.endsWith('`')) { - result = text.slice(1, -1); - result = result.replace(/``/g, '`'); - result = result.replace(/\\`/g, '`'); - } else if (text.startsWith('{') && text.endsWith('}')) { - result = text.slice(1, -1); - result = result.replace(/{{/g, '{'); - result = result.replace(/\\{/g, '{'); - } else { - throw new SyntaxError(`Invalid string literal, must start and end with the same quote type: ${text}`); - } + /** Converts a string received from antlr via ctx.getText() into a JavaScript string */ + let result: string; - return replaceCommonEscapeCharacters(result); + if (text.startsWith("'") && text.endsWith("'")) { + result = text.slice(1, -1); + result = result.replace(/''/g, "'"); + result = result.replace(/\\'/g, "'"); + } else if (text.startsWith('"') && text.endsWith('"')) { + result = text.slice(1, -1); + result = result.replace(/""/g, '"'); + result = result.replace(/\\"/g, '"'); + } else if (text.startsWith("`") && text.endsWith("`")) { + result = text.slice(1, -1); + result = result.replace(/``/g, "`"); + result = result.replace(/\\`/g, "`"); + } else if (text.startsWith("{") && text.endsWith("}")) { + result = text.slice(1, -1); + result = result.replace(/{{/g, "{"); + result = result.replace(/\\{/g, "{"); + } else { + throw new SyntaxError( + `Invalid string literal, must start and end with the same quote type: ${text}` + ); + } + + 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); + /** 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); +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/parser.ts b/internal-packages/tsql/src/query/parser.ts index 05b1df052b..a4be2fbb65 100644 --- a/internal-packages/tsql/src/query/parser.ts +++ b/internal-packages/tsql/src/query/parser.ts @@ -1689,9 +1689,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor { visitTsqlxTagElementClosed(ctx: TSQLxTagElementContext): TSQLXTag { const kind = this.visitIdentifier(ctx.identifier()[0]); const attributes = ctx.tSQLxTagAttribute() - ? ctx - .tSQLxTagAttribute() - .map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a)) + ? ctx.tSQLxTagAttribute().map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a)) : []; return { expression_type: "tsqlx_tag", kind, attributes }; } @@ -1706,9 +1704,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor { } const attributes = ctx.tSQLxTagAttribute() - ? ctx - .tSQLxTagAttribute() - .map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a)) + ? ctx.tSQLxTagAttribute().map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a)) : []; // ── collect child nodes, discarding pure-indentation whitespace ── diff --git a/internal-packages/tsql/src/query/printer.test.ts b/internal-packages/tsql/src/query/printer.test.ts index 88ce733035..0aa5b81606 100644 --- a/internal-packages/tsql/src/query/printer.test.ts +++ b/internal-packages/tsql/src/query/printer.test.ts @@ -2,12 +2,7 @@ import { describe, it, expect, beforeEach } from "vitest"; import { parseTSQLSelect, parseTSQLExpr, compileTSQL } from "../index.js"; import { ClickHousePrinter, printToClickHouse, type PrintResult } from "./printer.js"; import { createPrinterContext, PrinterContext } from "./printer_context.js"; -import { - createSchemaRegistry, - column, - type TableSchema, - type SchemaRegistry, -} from "./schema.js"; +import { createSchemaRegistry, column, type TableSchema, type SchemaRegistry } from "./schema.js"; import type { BucketThreshold } from "./time_buckets.js"; import { QueryError, SyntaxError } from "./errors.js"; @@ -253,17 +248,17 @@ describe("ClickHousePrinter", () => { describe("Table and column name mapping", () => { function createMappedContext() { const schema = createSchemaRegistry([runsSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); - } + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } - it("should map user-friendly table name to ClickHouse name", () => { + it("should map user-friendly table name to ClickHouse name", () => { const ctx = createMappedContext(); const { sql } = printQuery("SELECT * FROM runs", ctx); @@ -486,17 +481,17 @@ describe("ClickHousePrinter", () => { function createJsonContext() { const schema = createSchemaRegistry([jsonSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); - } + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } - it("should transform IS NULL to equals empty object for JSON columns with nullValue", () => { + it("should transform IS NULL to equals empty object for JSON columns with nullValue", () => { const ctx = createJsonContext(); const { sql } = printQuery("SELECT * FROM runs WHERE error IS NULL", ctx); @@ -616,10 +611,7 @@ describe("ClickHousePrinter", () => { it("should NOT add .:String type hint for JSON subfield in WHERE comparison", () => { const ctx = createJsonContext(); - const { sql } = printQuery( - "SELECT id FROM runs WHERE error.data.name = 'test'", - ctx - ); + const { sql } = printQuery("SELECT id FROM runs WHERE error.data.name = 'test'", ctx); // WHERE clause should NOT have .:String type hint (it breaks the query) expect(sql).toContain("equals(error.data.name,"); @@ -628,10 +620,7 @@ describe("ClickHousePrinter", () => { it("should NOT add .:String for JSON subfield in WHERE with LIKE", () => { const ctx = createJsonContext(); - const { sql } = printQuery( - "SELECT id FROM runs WHERE error.message LIKE '%error%'", - ctx - ); + const { sql } = printQuery("SELECT id FROM runs WHERE error.message LIKE '%error%'", ctx); // WHERE clause should NOT have .:String type hint expect(sql).toContain("like(error.message,"); @@ -702,18 +691,18 @@ describe("ClickHousePrinter", () => { function createTextColumnContext() { const schema = createSchemaRegistry([textColumnSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); - } + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } - describe("SELECT clause", () => { - it("should use text column when selecting bare JSON column", () => { + describe("SELECT clause", () => { + it("should use text column when selecting bare JSON column", () => { const ctx = createTextColumnContext(); const { sql } = printQuery("SELECT output FROM runs", ctx); @@ -791,10 +780,7 @@ describe("ClickHousePrinter", () => { it("should use JSON column for subfield comparison without .:String", () => { const ctx = createTextColumnContext(); - const { sql } = printQuery( - "SELECT id FROM runs WHERE output.data.name = 'test'", - ctx - ); + const { sql } = printQuery("SELECT id FROM runs WHERE output.data.name = 'test'", ctx); // Should use the original JSON column, not the text column // And should NOT have .:String in WHERE (breaks the query) @@ -847,10 +833,7 @@ describe("ClickHousePrinter", () => { it("should use text column in both SELECT and WHERE for same query", () => { const ctx = createTextColumnContext(); - const { sql } = printQuery( - "SELECT output FROM runs WHERE output LIKE '%test%'", - ctx - ); + const { sql } = printQuery("SELECT output FROM runs WHERE output LIKE '%test%'", ctx); // SELECT should use text column expect(sql).toContain("output_text AS output"); @@ -991,18 +974,18 @@ describe("ClickHousePrinter", () => { function createDataPrefixContext() { const schema = createSchemaRegistry([dataPrefixSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); - } + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } - describe("SELECT clause", () => { - it("should inject dataPrefix into JSON subfield path", () => { + describe("SELECT clause", () => { + it("should inject dataPrefix into JSON subfield path", () => { const ctx = createDataPrefixContext(); const { sql } = printQuery("SELECT output.message FROM runs", ctx); @@ -1017,9 +1000,7 @@ describe("ClickHousePrinter", () => { // Alias should be output_message, not output_data_message expect(sql).toContain("AS output_message"); expect(sql).not.toContain("AS output_data_message"); - expect(columns).toContainEqual( - expect.objectContaining({ name: "output_message" }) - ); + expect(columns).toContainEqual(expect.objectContaining({ name: "output_message" })); }); it("should handle nested paths with dataPrefix", () => { @@ -1055,10 +1036,7 @@ describe("ClickHousePrinter", () => { describe("WHERE clause", () => { it("should inject dataPrefix into WHERE comparison", () => { const ctx = createDataPrefixContext(); - const { sql } = printQuery( - "SELECT id FROM runs WHERE output.status = 'success'", - ctx - ); + const { sql } = printQuery("SELECT id FROM runs WHERE output.status = 'success'", ctx); // Should transform output.status to output.data.status expect(sql).toContain("output.data.status"); @@ -1066,10 +1044,7 @@ describe("ClickHousePrinter", () => { it("should inject dataPrefix into LIKE comparison", () => { const ctx = createDataPrefixContext(); - const { sql } = printQuery( - "SELECT id FROM runs WHERE error.message LIKE '%failed%'", - ctx - ); + const { sql } = printQuery("SELECT id FROM runs WHERE error.message LIKE '%failed%'", ctx); expect(sql).toContain("error.data.message"); }); @@ -1252,7 +1227,9 @@ describe("ClickHousePrinter", () => { describe("Date functions with interval units", () => { it("should output dateAdd with string interval as bare keyword", () => { - const { sql } = printQuery("SELECT dateAdd('day', 7, created_at) AS week_later FROM task_runs"); + const { sql } = printQuery( + "SELECT dateAdd('day', 7, created_at) AS week_later FROM task_runs" + ); expect(sql).toContain("dateAdd(day, 7, created_at)"); expect(sql).not.toContain("'day'"); @@ -1308,7 +1285,9 @@ describe("ClickHousePrinter", () => { }); it("should handle case-insensitive interval units", () => { - const { sql } = printQuery("SELECT dateAdd('DAY', 7, created_at) AS week_later FROM task_runs"); + const { sql } = printQuery( + "SELECT dateAdd('DAY', 7, created_at) AS week_later FROM task_runs" + ); expect(sql).toContain("dateAdd(day, 7, created_at)"); }); @@ -1659,17 +1638,17 @@ describe("Value mapping (valueMap)", () => { function createValueMapContext() { const schema = createSchemaRegistry([statusMappedSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); -} + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } -it("should transform user-friendly value to internal value in equality comparison", () => { + it("should transform user-friendly value to internal value in equality comparison", () => { const ctx = createValueMapContext(); const { sql, params } = printQuery("SELECT * FROM runs WHERE status = 'Completed'", ctx); @@ -1777,17 +1756,17 @@ describe("WHERE transform (whereTransform)", () => { function createPrefixedContext() { const schema = createSchemaRegistry([prefixedIdSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test123" }, - project_id: { op: "eq", value: "proj_test456" }, - environment_id: { op: "eq", value: "env_test789" }, - }, - }); -} + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test123" }, + project_id: { op: "eq", value: "proj_test456" }, + environment_id: { op: "eq", value: "env_test789" }, + }, + }); + } -it("should strip prefix from value in equality comparison", () => { + it("should strip prefix from value in equality comparison", () => { const ctx = createPrefixedContext(); const { params } = printQuery("SELECT * FROM runs WHERE batch_id = 'batch_abc123'", ctx); @@ -2003,18 +1982,18 @@ describe("Virtual columns", () => { function createVirtualColumnContext() { const schema = createSchemaRegistry([virtualColumnSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); -} + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } -describe("SELECT clause", () => { - it("should expand bare virtual column to expression with alias", () => { + describe("SELECT clause", () => { + it("should expand bare virtual column to expression with alias", () => { const ctx = createVirtualColumnContext(); const { sql } = printQuery("SELECT execution_duration FROM runs", ctx); @@ -2246,18 +2225,18 @@ describe("Expression columns with division (cost/invocation_cost pattern)", () = function createCostExpressionContext() { const schema = createSchemaRegistry([costExpressionSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); -} + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } -describe("WHERE clause with division expression columns", () => { - it("should expand invocation_cost > 100 to (base_cost_in_cents / 100.0) > 100", () => { + describe("WHERE clause with division expression columns", () => { + it("should expand invocation_cost > 100 to (base_cost_in_cents / 100.0) > 100", () => { const ctx = createCostExpressionContext(); const { sql } = printQuery("SELECT * FROM runs WHERE invocation_cost > 100", ctx); @@ -2392,18 +2371,18 @@ describe("Column metadata", () => { function createMetadataTestContext() { const schema = createSchemaRegistry([schemaWithRenderTypes]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); -} + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } -describe("Basic column metadata", () => { - it("should return column metadata for simple field references", () => { + describe("Basic column metadata", () => { + it("should return column metadata for simple field references", () => { const ctx = createMetadataTestContext(); const { columns } = printQuery("SELECT run_id, created_at FROM runs", ctx); @@ -2835,16 +2814,10 @@ describe("Basic column metadata", () => { it("should throw for invalid format type", () => { const ctx = createMetadataTestContext(); expect(() => { - printQuery( - "SELECT prettyFormat(usage_duration_ms, 'invalid') FROM runs", - ctx - ); + printQuery("SELECT prettyFormat(usage_duration_ms, 'invalid') FROM runs", ctx); }).toThrow(QueryError); expect(() => { - printQuery( - "SELECT prettyFormat(usage_duration_ms, 'invalid') FROM runs", - ctx - ); + printQuery("SELECT prettyFormat(usage_duration_ms, 'invalid') FROM runs", ctx); }).toThrow(/Unknown format type/); }); @@ -2861,16 +2834,10 @@ describe("Basic column metadata", () => { it("should throw when second argument is not a string literal", () => { const ctx = createMetadataTestContext(); expect(() => { - printQuery( - "SELECT prettyFormat(usage_duration_ms, 123) FROM runs", - ctx - ); + printQuery("SELECT prettyFormat(usage_duration_ms, 123) FROM runs", ctx); }).toThrow(QueryError); expect(() => { - printQuery( - "SELECT prettyFormat(usage_duration_ms, 123) FROM runs", - ctx - ); + printQuery("SELECT prettyFormat(usage_duration_ms, 123) FROM runs", ctx); }).toThrow(/must be a string literal/); }); @@ -2890,10 +2857,7 @@ describe("Basic column metadata", () => { it("should auto-populate format from customRenderType when not explicitly set", () => { const ctx = createMetadataTestContext(); - const { columns } = printQuery( - "SELECT usage_duration_ms, cost_in_cents FROM runs", - ctx - ); + const { columns } = printQuery("SELECT usage_duration_ms, cost_in_cents FROM runs", ctx); expect(columns).toHaveLength(2); // customRenderType should auto-populate format @@ -3359,7 +3323,9 @@ describe("Internal-only column blocking", () => { }); it("should allow grouping by exposed tenant column", () => { - const { sql } = printQuery("SELECT organization_id, count(*) FROM task_runs GROUP BY organization_id"); + const { sql } = printQuery( + "SELECT organization_id, count(*) FROM task_runs GROUP BY organization_id" + ); expect(sql).toContain("GROUP BY organization_id"); }); }); @@ -3507,9 +3473,9 @@ describe("Required Filters", () => { `); // All should work without errors - expect(sql).toContain("friendly_id"); // run_id maps to friendly_id + expect(sql).toContain("friendly_id"); // run_id maps to friendly_id expect(sql).toContain("status"); - expect(sql).toContain("created_at"); // triggered_at maps to created_at + expect(sql).toContain("created_at"); // triggered_at maps to created_at expect(sql).toContain("cost_in_cents"); // total_cost is a virtual column }); }); @@ -3681,9 +3647,9 @@ describe("timeBucket()", () => { describe("error handling", () => { it("should throw when timeBucket() is called with arguments", () => { - expect(() => - printTimeBucketQuery("SELECT timeBucket(triggered_at) FROM runs") - ).toThrow("timeBucket() does not accept arguments"); + expect(() => printTimeBucketQuery("SELECT timeBucket(triggered_at) FROM runs")).toThrow( + "timeBucket() does not accept arguments" + ); }); it("should throw when table has no timeConstraint", () => { @@ -3828,9 +3794,7 @@ describe("timeBucket()", () => { timeRange: threeMinuteRange, }); - const ast = parseTSQLSelect( - "SELECT timeBucket(), count() FROM metrics GROUP BY timeBucket" - ); + const ast = parseTSQLSelect("SELECT timeBucket(), count() FROM metrics GROUP BY timeBucket"); const { sql } = printToClickHouse(ast, ctx); // Custom thresholds: under 10 min β†’ 10 SECOND (not the global 5 SECOND) @@ -3855,9 +3819,7 @@ describe("timeBucket()", () => { timeRange: threeMinuteRange, }); - const ast = parseTSQLSelect( - "SELECT timeBucket(), count() FROM runs GROUP BY timeBucket" - ); + const ast = parseTSQLSelect("SELECT timeBucket(), count() FROM runs GROUP BY timeBucket"); const { sql } = printToClickHouse(ast, ctx); // Global default: under 5 min β†’ 5 SECOND diff --git a/internal-packages/tsql/src/query/printer.ts b/internal-packages/tsql/src/query/printer.ts index d45002f671..a4b50ca854 100644 --- a/internal-packages/tsql/src/query/printer.ts +++ b/internal-packages/tsql/src/query/printer.ts @@ -741,7 +741,9 @@ export class ClickHousePrinter { } // Set format hint from prettyFormat() or auto-populate from customRenderType - const sourceWithFormat = sourceColumn as (Partial & { format?: ColumnFormatType }) | null; + const sourceWithFormat = sourceColumn as + | (Partial & { format?: ColumnFormatType }) + | null; if (sourceWithFormat?.format) { metadata.format = sourceWithFormat.format; } else if (sourceColumn?.customRenderType) { @@ -2990,10 +2992,7 @@ export class ClickHousePrinter { private visitCallArgs(functionName: string, args: Expression[]): string[] { const lowerName = functionName.toLowerCase(); - if ( - ClickHousePrinter.DATE_FUNCTIONS_WITH_INTERVAL_UNIT.has(lowerName) && - args.length > 0 - ) { + if (ClickHousePrinter.DATE_FUNCTIONS_WITH_INTERVAL_UNIT.has(lowerName) && args.length > 0) { const firstArg = args[0]; const intervalUnit = this.extractIntervalUnit(firstArg); diff --git a/internal-packages/tsql/src/query/results.test.ts b/internal-packages/tsql/src/query/results.test.ts index 49216992cf..d4670ee19f 100644 --- a/internal-packages/tsql/src/query/results.test.ts +++ b/internal-packages/tsql/src/query/results.test.ts @@ -94,9 +94,7 @@ describe("transformResults", () => { }); it("should not modify columns without valueMap", () => { - const rows = [ - { id: "run_1", status: "COMPLETED_SUCCESSFULLY", task_identifier: "my-task" }, - ]; + const rows = [{ id: "run_1", status: "COMPLETED_SUCCESSFULLY", task_identifier: "my-task" }]; const transformed = transformResults(rows, [taskRunsSchema]); @@ -231,4 +229,3 @@ describe("createResultTransformer", () => { expect(transformed).toBe(rows); }); }); - diff --git a/internal-packages/tsql/src/query/results.ts b/internal-packages/tsql/src/query/results.ts index 8edeb8f54e..c26702e583 100644 --- a/internal-packages/tsql/src/query/results.ts +++ b/internal-packages/tsql/src/query/results.ts @@ -198,4 +198,3 @@ export function createResultTransformer( return rows.map((row) => transformRow(row, columnTransformMaps, options.fieldMappings)); }; } - diff --git a/internal-packages/tsql/src/query/schema.ts b/internal-packages/tsql/src/query/schema.ts index 615d112c2f..9a1e2d2ddf 100644 --- a/internal-packages/tsql/src/query/schema.ts +++ b/internal-packages/tsql/src/query/schema.ts @@ -869,7 +869,11 @@ export function sanitizeErrorMessage(message: string, schemas: TableSchema[]): s // Collect tenant column names to strip (global tables have no tenant columns) const tenantCols = table.tenantColumns; if (tenantCols) { - columnsToStrip.push(tenantCols.organizationId, tenantCols.projectId, tenantCols.environmentId); + columnsToStrip.push( + tenantCols.organizationId, + tenantCols.projectId, + tenantCols.environmentId + ); } // Collect required filter columns to strip diff --git a/internal-packages/tsql/src/query/security.test.ts b/internal-packages/tsql/src/query/security.test.ts index 2fcca47777..8c18981ee4 100644 --- a/internal-packages/tsql/src/query/security.test.ts +++ b/internal-packages/tsql/src/query/security.test.ts @@ -614,11 +614,11 @@ describe("Multi-join Tenant Guard Qualification", () => { // The guards should be table-qualified to prevent binding to the wrong table // Look for pattern like: r.organization_id and e.organization_id (with table alias prefix) // The exact format in ClickHouse SQL is just "alias.column" after resolution - + // Count qualified organization_id references (should have table prefixes) // In the WHERE clause, we should see both r.organization_id and e.organization_id const whereClause = sql.substring(sql.indexOf("WHERE")); - + // Both tables should have their own qualified tenant guards // The pattern should be: table_alias.organization_id for each table expect(whereClause).toMatch(/\br\b[^,]*organization_id/); @@ -633,7 +633,7 @@ describe("Multi-join Tenant Guard Qualification", () => { `); const whereClause = sql.substring(sql.indexOf("WHERE")); - + // Both tables should have qualified guards expect(whereClause).toMatch(/\br\b[^,]*organization_id/); expect(whereClause).toMatch(/\be\b[^,]*organization_id/); @@ -648,7 +648,7 @@ describe("Multi-join Tenant Guard Qualification", () => { `); const whereClause = sql.substring(sql.indexOf("WHERE")); - + // All three table aliases should have qualified guards expect(whereClause).toMatch(/\br\b[^,]*organization_id/); expect(whereClause).toMatch(/\be1\b[^,]*organization_id/); @@ -667,13 +667,13 @@ describe("Multi-join Tenant Guard Qualification", () => { // This ensures each table gets its own guard, not shared/ambiguous references const orgIdPattern = /(\w+)\.organization_id/g; const matches = [...sql.matchAll(orgIdPattern)]; - const tableAliases = matches.map(m => m[1]); - + const tableAliases = matches.map((m) => m[1]); + // Should have at least 2 different table aliases for organization_id // (one for task_runs alias 'r' and one for task_events alias 'e') expect(tableAliases).toContain("r"); expect(tableAliases).toContain("e"); - + // Both should use the same tenant value (parameterized) expect(Object.values(params)).toContain("org_tenant1"); }); diff --git a/internal-packages/tsql/src/query/timings.ts b/internal-packages/tsql/src/query/timings.ts index d868fe0667..32384e7fbe 100644 --- a/internal-packages/tsql/src/query/timings.ts +++ b/internal-packages/tsql/src/query/timings.ts @@ -8,22 +8,22 @@ * - Node.js <18 via perf_hooks module */ function getPerformanceNow(): number { - // Check for global performance (Node.js 18+ or browser) - if (typeof globalThis !== 'undefined' && 'performance' in globalThis) { - const perf = (globalThis as any).performance; - if (perf && typeof perf.now === 'function') { - return perf.now(); - } + // Check for global performance (Node.js 18+ or browser) + if (typeof globalThis !== "undefined" && "performance" in globalThis) { + const perf = (globalThis as any).performance; + if (perf && typeof perf.now === "function") { + return perf.now(); } - - // Fallback to Date.now() if performance API is not available - // Note: This is less precise but works everywhere - return Date.now(); + } + + // Fallback to Date.now() if performance API is not available + // Note: This is less precise but works everywhere + return Date.now(); } export interface QueryTiming { - key: string; // Key identifying the timing measurement - time: number; // Time in seconds + key: string; // Key identifying the timing measurement + time: number; // Time in seconds } const TIMING_DECIMAL_PLACES = 3; // round to milliseconds @@ -31,77 +31,79 @@ const TIMING_DECIMAL_PLACES = 3; // round to milliseconds // Not thread safe. // See trends_query_runner for an example of how to use for multithreaded queries export class TSQLTimings { - // Completed time in seconds for different parts of the TSQL query - timings: Record = {}; + // Completed time in seconds for different parts of the TSQL query + timings: Record = {}; - // Used for housekeeping - private _timingPointer: string; - private _timingStarts: Record = {}; + // Used for housekeeping + private _timingPointer: string; + private _timingStarts: Record = {}; - constructor(_timingPointer: string = '.') { - this._timingPointer = _timingPointer; - this._timingStarts[this._timingPointer] = this.perfCounter(); - } + constructor(_timingPointer: string = ".") { + this._timingPointer = _timingPointer; + this._timingStarts[this._timingPointer] = this.perfCounter(); + } - cloneForSubquery(seriesIndex: number): TSQLTimings { - return new TSQLTimings(`${this._timingPointer}/series_${seriesIndex}`); - } + cloneForSubquery(seriesIndex: number): TSQLTimings { + return new TSQLTimings(`${this._timingPointer}/series_${seriesIndex}`); + } - clearTimings(): void { - this.timings = {}; - } + clearTimings(): void { + this.timings = {}; + } - /** - * Measure execution time of a function. - * Usage: timings.measure('operation', () => { ... }); - */ - measure(key: string, fn: () => T): T { - const lastKey = this._timingPointer; - const fullKey = `${this._timingPointer}/${key}`; - this._timingPointer = fullKey; - this._timingStarts[fullKey] = this.perfCounter(); + /** + * Measure execution time of a function. + * Usage: timings.measure('operation', () => { ... }); + */ + measure(key: string, fn: () => T): T { + const lastKey = this._timingPointer; + const fullKey = `${this._timingPointer}/${key}`; + this._timingPointer = fullKey; + this._timingStarts[fullKey] = this.perfCounter(); - try { - return fn(); - } finally { - const duration = (this.perfCounter() - this._timingStarts[fullKey]) / 1000; // Convert to seconds - this.timings[fullKey] = (this.timings[fullKey] || 0.0) + duration; - delete this._timingStarts[fullKey]; - this._timingPointer = lastKey; - } + try { + return fn(); + } finally { + const duration = (this.perfCounter() - this._timingStarts[fullKey]) / 1000; // Convert to seconds + this.timings[fullKey] = (this.timings[fullKey] || 0.0) + duration; + delete this._timingStarts[fullKey]; + this._timingPointer = lastKey; } + } - /** - * Get performance counter in milliseconds (Node.js equivalent of perf_counter) - */ - private perfCounter(): number { - return getPerformanceNow(); - } + /** + * Get performance counter in milliseconds (Node.js equivalent of perf_counter) + */ + private perfCounter(): number { + return getPerformanceNow(); + } - toDict(): Record { - const timings = { ...this.timings }; - // Process in reverse order to handle nested timings correctly - const keys = Object.keys(this._timingStarts).reverse(); - for (const key of keys) { - const start = this._timingStarts[key]; - const elapsed = (this.perfCounter() - start) / 1000; // Convert to seconds - timings[key] = this.round((timings[key] || 0.0) + elapsed); - } - return timings; + toDict(): Record { + const timings = { ...this.timings }; + // Process in reverse order to handle nested timings correctly + const keys = Object.keys(this._timingStarts).reverse(); + for (const key of keys) { + const start = this._timingStarts[key]; + const elapsed = (this.perfCounter() - start) / 1000; // Convert to seconds + timings[key] = this.round((timings[key] || 0.0) + elapsed); } + return timings; + } - toList(backOutStack: boolean = true): QueryTiming[] { - const timingDict = backOutStack ? this.toDict() : this.timings; - return Object.entries(timingDict).map(([key, time]) => ({ - key: key, - time: this.round(time), - })); - } + toList(backOutStack: boolean = true): QueryTiming[] { + const timingDict = backOutStack ? this.toDict() : this.timings; + return Object.entries(timingDict).map(([key, time]) => ({ + key: key, + time: this.round(time), + })); + } - /** - * Round to specified decimal places (milliseconds precision) - */ - private round(value: number): number { - return Math.round(value * Math.pow(10, TIMING_DECIMAL_PLACES)) / Math.pow(10, TIMING_DECIMAL_PLACES); - } + /** + * Round to specified decimal places (milliseconds precision) + */ + private round(value: number): number { + return ( + Math.round(value * Math.pow(10, TIMING_DECIMAL_PLACES)) / Math.pow(10, TIMING_DECIMAL_PLACES) + ); + } } diff --git a/internal-packages/zod-worker/package.json b/internal-packages/zod-worker/package.json index 352fa94529..190dd1facd 100644 --- a/internal-packages/zod-worker/package.json +++ b/internal-packages/zod-worker/package.json @@ -19,4 +19,4 @@ "scripts": { "typecheck": "tsc --noEmit" } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 9c4b38c0aa..6bd4e3a556 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "dev": "turbo run dev", "i:dev": "infisical run -- turbo run dev", "generate": "turbo run generate", - "lint": "turbo run lint", + "format": "oxfmt .", + "lint": "oxlint", + "lint:fix": "oxlint --fix", "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", @@ -57,11 +59,10 @@ "@types/node": "20.14.14", "@vitest/coverage-v8": "4.1.7", "autoprefixer": "^10.4.12", - "eslint-plugin-turbo": "^2.0.4", - "lefthook": "^1.11.3", + "oxfmt": "^0.54.0", + "oxlint": "^1.69.0", "pkg-pr-new": "0.0.75", "pkg-types": "1.1.3", - "prettier": "^3.0.0", "tsx": "^3.7.1", "turbo": "^1.10.3", "typescript": "5.5.4", @@ -143,7 +144,6 @@ "better-sqlite3", "cpu-features", "esbuild", - "lefthook", "prisma", "protobufjs", "sharp", diff --git a/packages/build/src/extensions/core/neonSyncEnvVars.ts b/packages/build/src/extensions/core/neonSyncEnvVars.ts index 2e0dde3143..257eaed527 100644 --- a/packages/build/src/extensions/core/neonSyncEnvVars.ts +++ b/packages/build/src/extensions/core/neonSyncEnvVars.ts @@ -85,8 +85,7 @@ export function syncNeonEnvVars(options?: { envVarPrefix?: string; }): BuildExtension { const sync = syncEnvVars(async (ctx) => { - const projectId = - options?.projectId ?? process.env.NEON_PROJECT_ID ?? ctx.env.NEON_PROJECT_ID; + const projectId = options?.projectId ?? process.env.NEON_PROJECT_ID ?? ctx.env.NEON_PROJECT_ID; const neonAccessToken = options?.neonAccessToken ?? process.env.NEON_ACCESS_TOKEN ?? ctx.env.NEON_ACCESS_TOKEN; const branch = options?.branch ?? ctx.branch; diff --git a/packages/build/src/extensions/core/syncSupabaseEnvVars.ts b/packages/build/src/extensions/core/syncSupabaseEnvVars.ts index f373c1e633..d90d27a763 100644 --- a/packages/build/src/extensions/core/syncSupabaseEnvVars.ts +++ b/packages/build/src/extensions/core/syncSupabaseEnvVars.ts @@ -133,9 +133,7 @@ export function syncSupabaseEnvVars(options?: { // Step 1: List branches const branchesUrl = `https://api.supabase.com/v1/projects/${projectId}/branches`; - const [branchesFetchError, branchesResponse] = await tryCatch( - fetch(branchesUrl, { headers }) - ); + const [branchesFetchError, branchesResponse] = await tryCatch(fetch(branchesUrl, { headers })); if (branchesFetchError) { throw new Error( @@ -170,9 +168,7 @@ export function syncSupabaseEnvVars(options?: { targetBranch = branches.find((b) => b.is_default); if (!targetBranch) { - throw new Error( - "syncSupabaseEnvVars: no default Supabase branch found for the project." - ); + throw new Error("syncSupabaseEnvVars: no default Supabase branch found for the project."); } } else { if (!branch) { @@ -218,9 +214,7 @@ export function syncSupabaseEnvVars(options?: { // Step 4: Get API keys for the branch project const apiKeysUrl = `https://api.supabase.com/v1/projects/${branchDetail.ref}/api-keys`; - const [apiKeysFetchError, apiKeysResponse] = await tryCatch( - fetch(apiKeysUrl, { headers }) - ); + const [apiKeysFetchError, apiKeysResponse] = await tryCatch(fetch(apiKeysUrl, { headers })); let anonKey: string | undefined; let serviceRoleKey: string | undefined; diff --git a/packages/build/src/extensions/core/vercelSyncEnvVars.ts b/packages/build/src/extensions/core/vercelSyncEnvVars.ts index ce79841e66..a3ff02ef51 100644 --- a/packages/build/src/extensions/core/vercelSyncEnvVars.ts +++ b/packages/build/src/extensions/core/vercelSyncEnvVars.ts @@ -36,7 +36,7 @@ export function syncVercelEnvVars(options?: { process.env.VERCEL_PREVIEW_BRANCH ?? ctx.env.VERCEL_PREVIEW_BRANCH ?? ctx.branch; - const isVercelEnv = !!(ctx.env.VERCEL); + const isVercelEnv = !!ctx.env.VERCEL; if (!projectId) { throw new Error( diff --git a/packages/build/src/internal/additionalFiles.ts b/packages/build/src/internal/additionalFiles.ts index 57a746c36b..a9e59363ca 100644 --- a/packages/build/src/internal/additionalFiles.ts +++ b/packages/build/src/internal/additionalFiles.ts @@ -1,10 +1,6 @@ import { BuildManifest } from "@trigger.dev/core/v3"; import { BuildContext } from "@trigger.dev/core/v3/build"; -import { - copyMatcherResults, - findFilesByMatchers, - type MatcherResult, -} from "./copyFiles.js"; +import { copyMatcherResults, findFilesByMatchers, type MatcherResult } from "./copyFiles.js"; export type AdditionalFilesOptions = { files: string[]; diff --git a/packages/cli-v3/e2e/fixtures/monorepo-react-email/.yarnrc.yml b/packages/cli-v3/e2e/fixtures/monorepo-react-email/.yarnrc.yml index 8b757b29a1..3186f3f079 100644 --- a/packages/cli-v3/e2e/fixtures/monorepo-react-email/.yarnrc.yml +++ b/packages/cli-v3/e2e/fixtures/monorepo-react-email/.yarnrc.yml @@ -1 +1 @@ -nodeLinker: node-modules \ No newline at end of file +nodeLinker: node-modules diff --git a/packages/cli-v3/e2e/utils.ts b/packages/cli-v3/e2e/utils.ts index 73530208c7..077ac3cb77 100644 --- a/packages/cli-v3/e2e/utils.ts +++ b/packages/cli-v3/e2e/utils.ts @@ -433,14 +433,14 @@ function parseAttributes(attributes: any): ExecuteTaskTraceEvent["attributes"] { acc[attribute.key] = isStringValue(attribute.value) ? attribute.value.stringValue : isIntValue(attribute.value) - ? Number(attribute.value.intValue) - : isDoubleValue(attribute.value) - ? attribute.value.doubleValue - : isBoolValue(attribute.value) - ? attribute.value.boolValue - : isBytesValue(attribute.value) - ? binaryToHex(attribute.value.bytesValue) - : undefined; + ? Number(attribute.value.intValue) + : isDoubleValue(attribute.value) + ? attribute.value.doubleValue + : isBoolValue(attribute.value) + ? attribute.value.boolValue + : isBytesValue(attribute.value) + ? binaryToHex(attribute.value.bytesValue) + : undefined; return acc; }, {}); diff --git a/packages/cli-v3/src/apiClient.ts b/packages/cli-v3/src/apiClient.ts index 7cf969a7bd..ac6c3ba985 100644 --- a/packages/cli-v3/src/apiClient.ts +++ b/packages/cli-v3/src/apiClient.ts @@ -320,11 +320,7 @@ export class CliApiClient { ); } - async archiveBranch( - projectRef: string, - env: UpsertBranchRequestBody["env"], - branch: string - ) { + async archiveBranch(projectRef: string, env: UpsertBranchRequestBody["env"], branch: string) { if (!this.accessToken) { throw new Error("archiveBranch: No access token"); } diff --git a/packages/cli-v3/src/build/bundleSkills.ts b/packages/cli-v3/src/build/bundleSkills.ts index 8533d254c7..9ddcf81f27 100644 --- a/packages/cli-v3/src/build/bundleSkills.ts +++ b/packages/cli-v3/src/build/bundleSkills.ts @@ -39,9 +39,7 @@ export type CopySkillFoldersOptions = { * and indirectly by the deploy path (via `bundleSkills` which discovers * skills via its own indexer pass first, then delegates here). */ -export async function copySkillFolders( - options: CopySkillFoldersOptions -): Promise { +export async function copySkillFolders(options: CopySkillFoldersOptions): Promise { const { skills, destinationRoot, workingDir, logger } = options; if (skills.length === 0) { @@ -49,9 +47,7 @@ export async function copySkillFolders( } for (const skill of skills) { - const callerDir = skill.filePath - ? resolvePath(workingDir, skill.filePath, "..") - : workingDir; + const callerDir = skill.filePath ? resolvePath(workingDir, skill.filePath, "..") : workingDir; const sourcePath = isAbsolute(skill.sourcePath) ? skill.sourcePath : resolvePath(callerDir, skill.sourcePath); @@ -101,9 +97,7 @@ export async function copySkillFolders( * No `trigger.config.ts` changes required β€” discovery is side-effect * based, same mechanism as task/prompt registration. */ -export async function bundleSkills( - options: BundleSkillsOptions -): Promise { +export async function bundleSkills(options: BundleSkillsOptions): Promise { const { buildManifest, buildManifestPath, workingDir, env, logger } = options; let skills: SkillManifest[]; diff --git a/packages/cli-v3/src/build/manifests.ts b/packages/cli-v3/src/build/manifests.ts index f1188233a5..5a1aa541a6 100644 --- a/packages/cli-v3/src/build/manifests.ts +++ b/packages/cli-v3/src/build/manifests.ts @@ -54,7 +54,10 @@ export async function copyManifestToDir( */ async function computeFileHash(filePath: string): Promise { const contents = await readFile(filePath); - return createHash("sha256").update(contents as Uint8Array).digest("hex").slice(0, 16); + return createHash("sha256") + .update(contents as Uint8Array) + .digest("hex") + .slice(0, 16); } /** diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 1ac161d3e4..c0d5d4627d 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -1214,10 +1214,10 @@ async function handleNativeBuildServerDeploy({ level === "error" ? chalk.bold(chalkError(message)) : level === "warn" - ? chalkWarning(message) - : level === "debug" - ? chalkGrey(message) - : message; + ? chalkWarning(message) + : level === "debug" + ? chalkGrey(message) + : message; // We use console.log here instead of clack's logger as the current version does not support changing the line spacing. // And the logs look verbose with the default spacing. diff --git a/packages/cli-v3/src/commands/dev.ts b/packages/cli-v3/src/commands/dev.ts index f1d4e30d88..3915cadc20 100644 --- a/packages/cli-v3/src/commands/dev.ts +++ b/packages/cli-v3/src/commands/dev.ts @@ -7,7 +7,12 @@ import { ResolvedConfig } from "@trigger.dev/core/v3/build"; import { Command, Option as CommandOption } from "commander"; import { z } from "zod"; import { CliApiClient } from "../apiClient.js"; -import { CommonCommandOptions, commonOptions, handleTelemetry, wrapCommandAction } from "../cli/common.js"; +import { + CommonCommandOptions, + commonOptions, + handleTelemetry, + wrapCommandAction, +} from "../cli/common.js"; import { watchConfig } from "../config.js"; import { DevSessionInstance, startDevSession } from "../dev/devSession.js"; import { createLockFile } from "../dev/lock.js"; @@ -214,7 +219,8 @@ export async function devCommand(options: DevCommandOptions) { ); } else { logger.log( - `${chalkError("X Error:")} You must login first. Use the \`login\` CLI command.\n\n${authorization.error + `${chalkError("X Error:")} You must login first. Use the \`login\` CLI command.\n\n${ + authorization.error }` ); } @@ -253,9 +259,9 @@ async function startDev(options: StartDevOptions) { const notificationPromise = options.skipPlatformNotifications ? undefined : fetchPlatformNotification({ - apiClient, - projectRef: options.projectRef, - }); + apiClient, + projectRef: options.projectRef, + }); await printStandloneInitialBanner(true, options.profile); @@ -276,7 +282,6 @@ async function startDev(options: StartDevOptions) { printDevBanner(displayedUpdateMessage); - if (envVars.TRIGGER_PROJECT_REF) { logger.debug("Using project ref from env", { ref: envVars.TRIGGER_PROJECT_REF }); } @@ -342,7 +347,7 @@ async function startDev(options: StartDevOptions) { devInstance = await bootDevSession(watcher.config); - const waitUntilExit = async () => { }; + const waitUntilExit = async () => {}; return { watcher, @@ -371,7 +376,6 @@ async function devArchiveCommand(dir: string, options: unknown) { ); } - async function archiveDevBranchCommand(dir: string, options: DevArchiveCommandOptions) { intro(`Archiving dev branch`); @@ -429,11 +433,7 @@ async function archiveDevBranchCommand(dir: string, options: DevArchiveCommandOp return result; } -async function archiveDevBranch( - authorization: LoginResultOk, - branch: string, - project: string -) { +async function archiveDevBranch(authorization: LoginResultOk, branch: string, project: string) { const apiClient = new CliApiClient(authorization.auth.apiUrl, authorization.auth.accessToken); const result = await apiClient.archiveBranch(project, "development", branch); diff --git a/packages/cli-v3/src/commands/init.ts b/packages/cli-v3/src/commands/init.ts index 8513c79854..f13f88d23a 100644 --- a/packages/cli-v3/src/commands/init.ts +++ b/packages/cli-v3/src/commands/init.ts @@ -107,7 +107,10 @@ Examples: "Additional arguments to pass to the package manager, accepts CSV for multiple args" ) .option("-y, --yes", "Skip all prompts and use defaults (requires --project-ref)") - .option("--no-browser", "Don't automatically open the browser during login; print the URL only") + .option( + "--no-browser", + "Don't automatically open the browser during login; print the URL only" + ) ) .addOption( new CommandOption( diff --git a/packages/cli-v3/src/commands/skills.ts b/packages/cli-v3/src/commands/skills.ts index b2d4613b02..f2290f3f05 100644 --- a/packages/cli-v3/src/commands/skills.ts +++ b/packages/cli-v3/src/commands/skills.ts @@ -63,10 +63,7 @@ export function configureSkillsCommand(program: Command) { "Choose the target (or targets) to install the Trigger.dev skills into. Native install is supported for: " + targets.join(", ") ) - .option( - "-y, --yes", - "Install all available skills for the selected targets without prompting" - ) + .option("-y, --yes", "Install all available skills for the selected targets without prompting") .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.", @@ -85,13 +82,18 @@ export function configureSkillsCommand(program: Command) { } export async function installSkillsCommand(options: unknown) { - return await wrapCommandAction("installSkillsCommand", SkillsCommandOptions, options, async (opts) => { - if (opts.logLevel) { - logger.loggerLevel = opts.logLevel; - } + return await wrapCommandAction( + "installSkillsCommand", + SkillsCommandOptions, + options, + async (opts) => { + if (opts.logLevel) { + logger.loggerLevel = opts.logLevel; + } - return await _installSkillsCommand(opts); - }); + return await _installSkillsCommand(opts); + } + ); } /** @@ -107,9 +109,9 @@ export async function installSkillsCommand(options: unknown) { * that resolves correctly both when bundled (`/dist/esm`) and from source * (`/src`, run via tsx in dev/tests). */ -export async function resolveBundledPackageJSON(startDir: string = sourceDir): Promise< - string | null -> { +export async function resolveBundledPackageJSON( + startDir: string = sourceDir +): Promise { let searchDir = startDir; for (let i = 0; i < 10; i++) { @@ -340,7 +342,9 @@ async function installSkillsForTarget( if (targetName === "unsupported") { // This should not happen as unsupported targets are handled separately, // but if it does, provide helpful output. - log.message(`${chalk.yellow("⚠")} Skipping unsupported target - see manual configuration above`); + log.message( + `${chalk.yellow("⚠")} Skipping unsupported target - see manual configuration above` + ); return; } @@ -427,7 +431,9 @@ type SkillsPointer = { file: string; mode: "region" | "dedicated" }; * shared (a marked block is upserted so we never clobber other content); "dedicated" * files are ours to own and overwrite. */ -function resolveSkillsPointerForTarget(targetName: (typeof targets)[number]): SkillsPointer | undefined { +function resolveSkillsPointerForTarget( + targetName: (typeof targets)[number] +): SkillsPointer | undefined { switch (targetName) { case "claude-code": { return { file: "CLAUDE.md", mode: "region" }; diff --git a/packages/cli-v3/src/config.ts b/packages/cli-v3/src/config.ts index 1421cafe06..5b5dbfdfad 100644 --- a/packages/cli-v3/src/config.ts +++ b/packages/cli-v3/src/config.ts @@ -118,8 +118,8 @@ export function configPlugin(resolvedConfig: ResolvedConfig): esbuild.Plugin | u ? $mod.exports.default.$args[0] : $mod.exports.default : $mod.exports.config?.$type === "function-call" - ? $mod.exports.config.$args[0] - : $mod.exports.config; + ? $mod.exports.config.$args[0] + : $mod.exports.config; options.build = {}; @@ -177,8 +177,8 @@ async function resolveConfig( const workingDir = result.configFile ? dirname(result.configFile) : packageJsonPath - ? dirname(packageJsonPath) - : cwd; + ? dirname(packageJsonPath) + : cwd; const config = "config" in result.config ? (result.config.config as TriggerConfig) : result.config; diff --git a/packages/cli-v3/src/dev/devOutput.ts b/packages/cli-v3/src/dev/devOutput.ts index 1ea302102f..93c6beed5c 100644 --- a/packages/cli-v3/src/dev/devOutput.ts +++ b/packages/cli-v3/src/dev/devOutput.ts @@ -92,7 +92,9 @@ export function startDevOutput(options: DevOutputOptions) { const runsLink = chalkLink(cliLink("View runs", runsUrl)); const runtime = chalkGrey(`[${worker.build.runtime}]`); - const workerStarted = chalkGrey(`Local worker ready on branch: ${branch ?? DEFAULT_DEV_BRANCH}`); + const workerStarted = chalkGrey( + `Local worker ready on branch: ${branch ?? DEFAULT_DEV_BRANCH}` + ); const workerVersion = chalkWorker(worker.serverWorker!.version); logParts.push(workerStarted, runtime, arrow, workerVersion); @@ -196,8 +198,8 @@ export function startDevOutput(options: DevOutputOptions) { !completion.ok && completion.skippedRetrying ? " (retrying skipped)" : !completion.ok && completion.retry !== undefined - ? ` (retrying in ${completion.retry.delay}ms)` - : "" + ? ` (retrying in ${completion.retry.delay}ms)` + : "" ); const resultText = !completion.ok @@ -211,8 +213,8 @@ export function startDevOutput(options: DevOutputOptions) { const errorText = !completion.ok ? formatErrorLog(completion.error) : "retry" in completion - ? `retry in ${completion.retry}ms` - : ""; + ? `retry in ${completion.retry}ms` + : ""; const elapsedText = chalkGrey( `(${formatDurationMilliseconds(durationMs, { style: "short" })})` diff --git a/packages/cli-v3/src/dev/devSupervisor.ts b/packages/cli-v3/src/dev/devSupervisor.ts index d2ca25928e..9521af8e02 100644 --- a/packages/cli-v3/src/dev/devSupervisor.ts +++ b/packages/cli-v3/src/dev/devSupervisor.ts @@ -1,5 +1,12 @@ import { spawn, type ChildProcess } from "node:child_process"; -import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from "node:fs"; +import { + readFileSync, + writeFileSync, + renameSync, + unlinkSync, + existsSync, + mkdirSync, +} from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { setTimeout as awaitTimeout } from "node:timers/promises"; @@ -83,7 +90,7 @@ class DevSupervisor implements WorkerRuntime { private activeRunsPath?: string; private watchdogPidPath?: string; - constructor(public readonly options: WorkerRuntimeOptions) { } + constructor(public readonly options: WorkerRuntimeOptions) {} async init(): Promise { logger.debug("[DevSupervisor] initialized worker runtime", { options: this.options }); @@ -124,10 +131,10 @@ class DevSupervisor implements WorkerRuntime { : false; const maxPoolSize = - typeof processKeepAlive === "object" ? processKeepAlive.devMaxPoolSize ?? 25 : 25; + typeof processKeepAlive === "object" ? (processKeepAlive.devMaxPoolSize ?? 25) : 25; const maxExecutionsPerProcess = - typeof processKeepAlive === "object" ? processKeepAlive.maxExecutionsPerProcess ?? 50 : 50; + typeof processKeepAlive === "object" ? (processKeepAlive.maxExecutionsPerProcess ?? 50) : 50; if (enableProcessReuse) { logger.debug("[DevSupervisor] Enabling process reuse", { @@ -288,10 +295,10 @@ class DevSupervisor implements WorkerRuntime { // Clean up files try { if (this.activeRunsPath) unlinkSync(this.activeRunsPath); - } catch { } + } catch {} try { if (this.watchdogPidPath) unlinkSync(this.watchdogPidPath); - } catch { } + } catch {} } #updateActiveRunsFile() { @@ -535,7 +542,6 @@ class DevSupervisor implements WorkerRuntime { taskRunProcessPool: this.taskRunProcessPool, cwd, onFinished: () => { - logger.debug("[DevSupervisor] Run finished", { runId: message.run.friendlyId }); //stop the run controller, and remove it @@ -910,4 +916,3 @@ function generateValidationIssueMessage( } } } - diff --git a/packages/cli-v3/src/entryPoints/dev-run-controller.ts b/packages/cli-v3/src/entryPoints/dev-run-controller.ts index ffa4228cbc..cf4038c126 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-controller.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-controller.ts @@ -132,7 +132,6 @@ export class DevRunController { }); } - // This should only be used when we're already executing a run. Attempt number changes are not allowed. private updateRunPhase(run: Run, snapshot: Snapshot) { if (this.state.phase !== "RUN") { diff --git a/packages/cli-v3/src/entryPoints/managed-index-worker.ts b/packages/cli-v3/src/entryPoints/managed-index-worker.ts index 4923beb3af..52e24cdd87 100644 --- a/packages/cli-v3/src/entryPoints/managed-index-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-index-worker.ts @@ -204,8 +204,8 @@ await sendMessageInCatalog( typeof processKeepAlive === "object" ? processKeepAlive : typeof processKeepAlive === "boolean" - ? { enabled: processKeepAlive } - : undefined, + ? { enabled: processKeepAlive } + : undefined, timings, }, importErrors, diff --git a/packages/cli-v3/src/executions/taskRunProcess.ts b/packages/cli-v3/src/executions/taskRunProcess.ts index 6816b2e24f..5db616ba85 100644 --- a/packages/cli-v3/src/executions/taskRunProcess.ts +++ b/packages/cli-v3/src/executions/taskRunProcess.ts @@ -213,9 +213,7 @@ export class TaskRunProcess { // expires β€” surfacing as TIMED_OUT/MAX_DURATION_EXCEEDED with empty attempts. Reject // any pending attempts now and gracefully terminate the worker so OTEL gets a flush // window before SIGKILL. - this.#rejectPendingAttempts( - new UncaughtExceptionError(message.error, message.origin) - ); + this.#rejectPendingAttempts(new UncaughtExceptionError(message.error, message.origin)); await this.#gracefullyTerminate(this.options.gracefulTerminationTimeoutInMs); }, @@ -317,11 +315,7 @@ export class TaskRunProcess { // @ts-expect-error - rejecter is assigned in the promise constructor above rejecter( - new UnexpectedExitError( - -1, - null, - "Child process is not connected, cannot execute task run" - ) + new UnexpectedExitError(-1, null, "Child process is not connected, cannot execute task run") ); } diff --git a/packages/cli-v3/src/mcp/config.ts b/packages/cli-v3/src/mcp/config.ts index 3e53137b9b..31b5c6ec4d 100644 --- a/packages/cli-v3/src/mcp/config.ts +++ b/packages/cli-v3/src/mcp/config.ts @@ -139,14 +139,12 @@ export const toolsMetadata = { whoami: { name: "whoami", title: "Who Am I", - description: - "Show the current authenticated user, active CLI profile, email, and API URL.", + description: "Show the current authenticated user, active CLI profile, email, and API URL.", }, list_profiles: { name: "list_profiles", title: "List Profiles", - description: - "List all configured CLI profiles. Shows which profile is currently active.", + description: "List all configured CLI profiles. Shows which profile is currently active.", }, switch_profile: { name: "switch_profile", diff --git a/packages/cli-v3/src/mcp/formatters.ts b/packages/cli-v3/src/mcp/formatters.ts index 131124cc9a..539762257d 100644 --- a/packages/cli-v3/src/mcp/formatters.ts +++ b/packages/cli-v3/src/mcp/formatters.ts @@ -473,10 +473,10 @@ export function formatSpanDetail(span: RetrieveSpanDetailResponseBody): string { const statusIndicator = span.isCancelled ? "[CANCELLED]" : span.isError - ? "[ERROR]" - : span.isPartial - ? "[IN PROGRESS]" - : "[COMPLETED]"; + ? "[ERROR]" + : span.isPartial + ? "[IN PROGRESS]" + : "[COMPLETED]"; lines.push(`## Span: ${span.message} ${statusIndicator}`); lines.push(`Span ID: ${span.spanId}`); diff --git a/packages/cli-v3/src/mcp/schemas.ts b/packages/cli-v3/src/mcp/schemas.ts index c69d11ab3e..01aa9577cb 100644 --- a/packages/cli-v3/src/mcp/schemas.ts +++ b/packages/cli-v3/src/mcp/schemas.ts @@ -242,17 +242,9 @@ export const QueryInput = CommonProjectsInput.extend({ period: z .string() .optional() - .describe( - "Time period shorthand, e.g. '1h', '7d', '30d'. Mutually exclusive with from/to." - ), - from: z - .string() - .optional() - .describe("Start of time range (ISO 8601). Must be paired with 'to'."), - to: z - .string() - .optional() - .describe("End of time range (ISO 8601). Must be paired with 'from'."), + .describe("Time period shorthand, e.g. '1h', '7d', '30d'. Mutually exclusive with from/to."), + from: z.string().optional().describe("Start of time range (ISO 8601). Must be paired with 'to'."), + to: z.string().optional().describe("End of time range (ISO 8601). Must be paired with 'from'."), }); export type QueryInput = z.output; @@ -265,9 +257,7 @@ export const QuerySchemaInput = CommonProjectsInput.pick({ }).extend({ table: z .string() - .describe( - "The table name to get the schema for (e.g. 'runs', 'metrics', 'llm_metrics')." - ), + .describe("The table name to get the schema for (e.g. 'runs', 'metrics', 'llm_metrics')."), }); export type QuerySchemaInput = z.output; @@ -296,14 +286,8 @@ export const RunDashboardQueryInput = CommonProjectsInput.extend({ .string() .optional() .describe("Time period shorthand, e.g. '1h', '7d', '30d'. Defaults to 1d."), - from: z - .string() - .optional() - .describe("Start of time range (ISO 8601). Must be paired with 'to'."), - to: z - .string() - .optional() - .describe("End of time range (ISO 8601). Must be paired with 'from'."), + from: z.string().optional().describe("Start of time range (ISO 8601). Must be paired with 'to'."), + to: z.string().optional().describe("End of time range (ISO 8601). Must be paired with 'from'."), scope: z .enum(["environment", "project", "organization"]) .default("environment") diff --git a/packages/cli-v3/src/mcp/smoke.test.ts b/packages/cli-v3/src/mcp/smoke.test.ts index d14403aa2c..b6d24bb4e3 100644 --- a/packages/cli-v3/src/mcp/smoke.test.ts +++ b/packages/cli-v3/src/mcp/smoke.test.ts @@ -75,10 +75,7 @@ async function main() { stderr: "pipe", }); - const client = new Client( - { name: "mcp-smoke-test", version: "1.0.0" }, - { capabilities: {} } - ); + const client = new Client({ name: "mcp-smoke-test", version: "1.0.0" }, { capabilities: {} }); await client.connect(transport); @@ -95,7 +92,13 @@ async function main() { const duration = Date.now() - start; if (isError) { - results.push({ tool: name, status: "fail", duration, error: preview(text), preview: preview(text) }); + results.push({ + tool: name, + status: "fail", + duration, + error: preview(text), + preview: preview(text), + }); return null; } @@ -166,10 +169,14 @@ async function main() { if (runsText) { const runMatch = runsText.match(/run_\w+/); if (runMatch) { - await test("get_run_details", { ...commonArgs, runId: runMatch[0], maxTraceLines: 10 }, (text) => { - assert(text.includes("Run Details"), "Expected run details header"); - assert(text.includes("Run Trace"), "Expected trace section"); - }); + await test( + "get_run_details", + { ...commonArgs, runId: runMatch[0], maxTraceLines: 10 }, + (text) => { + assert(text.includes("Run Details"), "Expected run details header"); + assert(text.includes("Run Trace"), "Expected trace section"); + } + ); } } @@ -198,11 +205,20 @@ async function main() { }); // 14. query - await test("query", { ...commonArgs, query: "SELECT status, count() as total FROM runs GROUP BY status ORDER BY total DESC LIMIT 5", period: "7d" }, (text) => { - assert(text.includes("Query Results"), "Expected results header"); - assert(text.includes("status"), "Expected status column"); - assert(!text.includes("```json"), "Should use text table, not JSON code block"); - }); + await test( + "query", + { + ...commonArgs, + query: + "SELECT status, count() as total FROM runs GROUP BY status ORDER BY total DESC LIMIT 5", + period: "7d", + }, + (text) => { + assert(text.includes("Query Results"), "Expected results header"); + assert(text.includes("status"), "Expected status column"); + assert(!text.includes("```json"), "Should use text table, not JSON code block"); + } + ); // 15. list_dashboards await test("list_dashboards", commonArgs, (text) => { @@ -211,9 +227,13 @@ async function main() { }); // 16. run_dashboard_query - await test("run_dashboard_query", { ...commonArgs, dashboardKey: "overview", widgetId: "9lDDdebQ", period: "7d" }, (text) => { - assert(text.includes("Total runs"), "Expected widget title"); - }); + await test( + "run_dashboard_query", + { ...commonArgs, dashboardKey: "overview", widgetId: "9lDDdebQ", period: "7d" }, + (text) => { + assert(text.includes("Total runs"), "Expected widget title"); + } + ); // 17. dev_server_status (should show stopped) await test("dev_server_status", {}, (text) => { @@ -234,7 +254,12 @@ async function main() { for (const r of results) { const icon = r.status === "pass" ? "βœ“" : r.status === "fail" ? "βœ—" : "β—‹"; const dur = `${r.duration}ms`.padStart(6); - const status = r.status === "pass" ? "\x1b[32mpass\x1b[0m" : r.status === "fail" ? "\x1b[31mfail\x1b[0m" : "\x1b[33mskip\x1b[0m"; + const status = + r.status === "pass" + ? "\x1b[32mpass\x1b[0m" + : r.status === "fail" + ? "\x1b[31mfail\x1b[0m" + : "\x1b[33mskip\x1b[0m"; console.log(` ${icon} ${r.tool.padEnd(25)} ${status} ${dur} ${r.error ?? r.preview ?? ""}`); diff --git a/packages/cli-v3/src/mcp/tools.test.ts b/packages/cli-v3/src/mcp/tools.test.ts index eef627fd5a..48d1488698 100644 --- a/packages/cli-v3/src/mcp/tools.test.ts +++ b/packages/cli-v3/src/mcp/tools.test.ts @@ -56,10 +56,7 @@ async function main() { stderr: "pipe", }); - const client = new Client( - { name: "mcp-test-cli", version: "1.0.0" }, - { capabilities: {} } - ); + const client = new Client({ name: "mcp-test-cli", version: "1.0.0" }, { capabilities: {} }); await client.connect(transport); diff --git a/packages/cli-v3/src/mcp/tools.ts b/packages/cli-v3/src/mcp/tools.ts index f438989258..a440750eca 100644 --- a/packages/cli-v3/src/mcp/tools.ts +++ b/packages/cli-v3/src/mcp/tools.ts @@ -30,11 +30,7 @@ import { reactivatePromptOverrideTool, } from "./tools/prompts.js"; import { listAgentsTool } from "./tools/agents.js"; -import { - startAgentChatTool, - sendAgentMessageTool, - closeAgentChatTool, -} from "./tools/agentChat.js"; +import { startAgentChatTool, sendAgentMessageTool, closeAgentChatTool } from "./tools/agentChat.js"; import { respondWithError } from "./utils.js"; /** Tool names that perform write/mutating operations. */ diff --git a/packages/cli-v3/src/mcp/tools/agentChat.ts b/packages/cli-v3/src/mcp/tools/agentChat.ts index 816d1d5f9d..f718722dd2 100644 --- a/packages/cli-v3/src/mcp/tools/agentChat.ts +++ b/packages/cli-v3/src/mcp/tools/agentChat.ts @@ -64,9 +64,7 @@ function serializeInputChunk(chunk: ChatInputChunk): string { const StartAgentChatInput = CommonProjectsInput.extend({ agentId: z .string() - .describe( - "The agent task ID to chat with. Use get_current_worker to see available agents." - ), + .describe("The agent task ID to chat with. Use get_current_worker to see available agents."), chatId: z .string() .describe("A unique conversation ID. Reuse to resume a conversation.") @@ -90,9 +88,7 @@ export const startAgentChatTool = { ctx.logger?.log("calling start_agent_chat", { input }); if (ctx.options.devOnly && input.environment !== "dev") { - return respondWithError( - `This MCP server is only available for the dev environment.` - ); + return respondWithError(`This MCP server is only available for the dev environment.`); } const projectRef = await ctx.getProjectRef({ @@ -103,12 +99,7 @@ export const startAgentChatTool = { const apiClient = await ctx.getApiClient({ projectRef, environment: input.environment, - scopes: [ - "write:tasks", - "read:runs", - "read:sessions", - "write:sessions", - ], + scopes: ["write:tasks", "read:runs", "read:sessions", "write:sessions"], branch: input.branch, }); @@ -210,7 +201,9 @@ export const sendAgentMessageTool = { const msgId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const userMessage: ChatMessage = { - id: msgId, role: "user", parts: [{ type: "text", text: input.message }], + id: msgId, + role: "user", + parts: [{ type: "text", text: input.message }], }; // Track the outgoing user message @@ -311,9 +304,7 @@ export const closeAgentChatTool = { const session = activeSessions.get(input.chatId); if (!session) { - return respondWithError( - `No active chat with ID "${input.chatId}".` - ); + return respondWithError(`No active chat with ID "${input.chatId}".`); } if (session.runId) { @@ -426,9 +417,7 @@ async function collectAgentResponse( // new run β€” reuse sessionId, swap runId. Slim-wire: ship only // the latest user message as the turn-N delta; prior turns // come back via snapshot+replay on the new run's boot. - const lastUserMessage = [...session.messages] - .reverse() - .find((m) => m.role === "user"); + const lastUserMessage = [...session.messages].reverse().find((m) => m.role === "user"); const previousRunId = session.runId; const result = await session.apiClient.triggerTask(session.agentId, { payload: { @@ -491,9 +480,7 @@ async function collectAgentResponse( if (chunk.type === "tool-output-available" && typeof chunk.toolCallId === "string") { // Update existing tool part with output - const toolPart = parts.find( - (p) => p.toolCallId === chunk.toolCallId - ); + const toolPart = parts.find((p) => p.toolCallId === chunk.toolCallId); if (toolPart) { toolPart.state = "output-available"; toolPart.output = chunk.output; @@ -516,9 +503,7 @@ async function collectAgentResponse( // ─── Response formatter ────────────────────────────────────────── -function formatAssistantParts( - parts: Array<{ type: string; [key: string]: unknown }> -): string { +function formatAssistantParts(parts: Array<{ type: string; [key: string]: unknown }>): string { const sections: string[] = []; for (const part of parts) { diff --git a/packages/cli-v3/src/mcp/tools/agents.ts b/packages/cli-v3/src/mcp/tools/agents.ts index e40bcafab6..f1e8c7bc7f 100644 --- a/packages/cli-v3/src/mcp/tools/agents.ts +++ b/packages/cli-v3/src/mcp/tools/agents.ts @@ -60,9 +60,7 @@ export const listAgentsTool = { contents.push( "Use `start_agent_chat` with an agent's slug as the `agentId` to start a conversation." ); - contents.push( - "Use `get_task_schema` with an agent's slug to see its payload schema." - ); + contents.push("Use `get_task_schema` with an agent's slug to see its payload schema."); return { content: [{ type: "text", text: contents.join("\n") }], diff --git a/packages/cli-v3/src/mcp/tools/deploys.ts b/packages/cli-v3/src/mcp/tools/deploys.ts index 2b071f44e9..9caba1114c 100644 --- a/packages/cli-v3/src/mcp/tools/deploys.ts +++ b/packages/cli-v3/src/mcp/tools/deploys.ts @@ -146,11 +146,15 @@ export const listDeploysTool = { for (const deploy of deploys) { const deployedAt = deploy.deployedAt - ? new Date(deploy.deployedAt).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC") + ? new Date(deploy.deployedAt) + .toISOString() + .replace("T", " ") + .replace(/\.\d+Z$/, " UTC") : "not deployed"; - const git = deploy.git && typeof deploy.git === "object" && "commitMessage" in deploy.git - ? ` | ${deploy.git.commitMessage}` - : ""; + const git = + deploy.git && typeof deploy.git === "object" && "commitMessage" in deploy.git + ? ` | ${deploy.git.commitMessage}` + : ""; lines.push( `- ${deploy.shortCode} | v${deploy.version} | ${deploy.status} | ${deployedAt}${git}` ); diff --git a/packages/cli-v3/src/mcp/tools/devServer.ts b/packages/cli-v3/src/mcp/tools/devServer.ts index 9a21477eda..f636b67cf5 100644 --- a/packages/cli-v3/src/mcp/tools/devServer.ts +++ b/packages/cli-v3/src/mcp/tools/devServer.ts @@ -217,10 +217,7 @@ export const devServerStatusTool = { const recentLogs = devLogs.slice(-input.lines).join("\n"); - const content = [ - `## Dev Server Status: ${devState}`, - "", - ]; + const content = [`## Dev Server Status: ${devState}`, ""]; if (devCwd) { content.push(`**Directory:** ${devCwd}`); diff --git a/packages/cli-v3/src/mcp/tools/profiles.ts b/packages/cli-v3/src/mcp/tools/profiles.ts index 56dfab7e33..0b02099cf2 100644 --- a/packages/cli-v3/src/mcp/tools/profiles.ts +++ b/packages/cli-v3/src/mcp/tools/profiles.ts @@ -71,9 +71,7 @@ export const listProfilesTool = { } content.push(""); - content.push( - "Use `switch_profile` to change the active profile for this session." - ); + content.push("Use `switch_profile` to change the active profile for this session."); return { content: [{ type: "text" as const, text: content.join("\n") }], diff --git a/packages/cli-v3/src/mcp/tools/prompts.ts b/packages/cli-v3/src/mcp/tools/prompts.ts index 7c23f64c24..e458f80c5c 100644 --- a/packages/cli-v3/src/mcp/tools/prompts.ts +++ b/packages/cli-v3/src/mcp/tools/prompts.ts @@ -36,8 +36,8 @@ const RemoveOverrideInput = PromptSlugInput; const ReactivateOverrideInput = CommonProjectsInput.extend({ slug: z.string().describe("The prompt slug"), - version: z - .coerce.number() + version: z.coerce + .number() .int() .positive() .describe("The dashboard-sourced version number to reactivate as override"), @@ -251,9 +251,7 @@ export const updatePromptOverrideTool = { }); return { - content: [ - { type: "text" as const, text: `Updated override for "${input.slug}".` }, - ], + content: [{ type: "text" as const, text: `Updated override for "${input.slug}".` }], }; }), }; @@ -285,9 +283,7 @@ export const removePromptOverrideTool = { await apiClient.removePromptOverride(input.slug); return { - content: [ - { type: "text" as const, text: `Removed override for "${input.slug}".` }, - ], + content: [{ type: "text" as const, text: `Removed override for "${input.slug}".` }], }; }), }; diff --git a/packages/cli-v3/src/mcp/tools/query.ts b/packages/cli-v3/src/mcp/tools/query.ts index e09cb9d904..709d1b8e90 100644 --- a/packages/cli-v3/src/mcp/tools/query.ts +++ b/packages/cli-v3/src/mcp/tools/query.ts @@ -144,9 +144,7 @@ export const getQuerySchemaTool = { if (!table) { const available = schema.tables.map((t) => `${t.name} (${t.description ?? ""})`).join(", "); - return respondWithError( - `Table "${input.table}" not found. Available tables: ${available}` - ); + return respondWithError(`Table "${input.table}" not found. Available tables: ${available}`); } const content = [formatSchemaTable(table)]; diff --git a/packages/cli-v3/src/mcp/tools/runs.ts b/packages/cli-v3/src/mcp/tools/runs.ts index d1d43df5f6..b4311bee4c 100644 --- a/packages/cli-v3/src/mcp/tools/runs.ts +++ b/packages/cli-v3/src/mcp/tools/runs.ts @@ -3,8 +3,20 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { toolsMetadata } from "../config.js"; -import { formatRun, formatRunList, formatRunShape, formatRunTrace, formatSpanDetail } from "../formatters.js"; -import { CommonRunsInput, GetRunDetailsInput, GetSpanDetailsInput, ListRunsInput, WaitForRunInput } from "../schemas.js"; +import { + formatRun, + formatRunList, + formatRunShape, + formatRunTrace, + formatSpanDetail, +} from "../formatters.js"; +import { + CommonRunsInput, + GetRunDetailsInput, + GetSpanDetailsInput, + ListRunsInput, + WaitForRunInput, +} from "../schemas.js"; import { respondWithError, toolHandler } from "../utils.js"; // Cache formatted traces in temp files keyed by runId. @@ -199,9 +211,7 @@ export const getSpanDetailsTool = { const spanDetail = await apiClient.retrieveSpan(input.runId, input.spanId); const formatted = formatSpanDetail(spanDetail); - const runUrl = await ctx.getDashboardUrl( - `/projects/v3/${projectRef}/runs/${input.runId}` - ); + const runUrl = await ctx.getDashboardUrl(`/projects/v3/${projectRef}/runs/${input.runId}`); const content = [formatted]; if (runUrl) { @@ -243,9 +253,7 @@ export const waitForRunToCompleteTool = { const timeoutMs = input.timeoutInSeconds * 1000; const timeoutSignal = AbortSignal.timeout(timeoutMs); - const combinedSignal = signal - ? AbortSignal.any([signal, timeoutSignal]) - : timeoutSignal; + const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal; const runSubscription = apiClient.subscribeToRun(input.runId, { signal: combinedSignal }); const readableStream = runSubscription.getReader(); diff --git a/packages/cli-v3/src/mcp/tools/tasks.ts b/packages/cli-v3/src/mcp/tools/tasks.ts index ffa39c4ca8..d7a01ecbeb 100644 --- a/packages/cli-v3/src/mcp/tools/tasks.ts +++ b/packages/cli-v3/src/mcp/tools/tasks.ts @@ -49,9 +49,7 @@ export const getCurrentWorker = { } contents.push(""); - contents.push( - "Use the `get_task_schema` tool with a task slug to get its payload schema." - ); + contents.push("Use the `get_task_schema` tool with a task slug to get its payload schema."); } else { contents.push(`The worker has no tasks registered.`); } @@ -198,16 +196,10 @@ export const getTaskSchemaTool = { if (!task) { const available = workerResult.data.worker.tasks.map((t) => t.slug).join(", "); - return respondWithError( - `Task "${input.taskSlug}" not found. Available tasks: ${available}` - ); + return respondWithError(`Task "${input.taskSlug}" not found. Available tasks: ${available}`); } - const content = [ - `## ${task.slug}`, - "", - `**File:** ${task.filePath}`, - ]; + const content = [`## ${task.slug}`, "", `**File:** ${task.filePath}`]; if (task.payloadSchema) { content.push(""); diff --git a/packages/cli-v3/src/rules/install.ts b/packages/cli-v3/src/rules/install.ts index 8b13789179..e69de29bb2 100644 --- a/packages/cli-v3/src/rules/install.ts +++ b/packages/cli-v3/src/rules/install.ts @@ -1 +0,0 @@ - diff --git a/packages/cli-v3/src/utilities/colorMarkup.test.ts b/packages/cli-v3/src/utilities/colorMarkup.test.ts index c6e64845ad..8acd237090 100644 --- a/packages/cli-v3/src/utilities/colorMarkup.test.ts +++ b/packages/cli-v3/src/utilities/colorMarkup.test.ts @@ -75,9 +75,21 @@ describe("applyColorMarkup", () => { it("handles all valid color tags", () => { const tags = [ - "red", "green", "yellow", "blue", "magenta", "cyan", "white", "gray", - "redBright", "greenBright", "yellowBright", "blueBright", - "magentaBright", "cyanBright", "whiteBright", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", + "gray", + "redBright", + "greenBright", + "yellowBright", + "blueBright", + "magentaBright", + "cyanBright", + "whiteBright", ]; for (const tag of tags) { const result = applyColorMarkup(`{${tag}}test{/${tag}}`); diff --git a/packages/cli-v3/src/utilities/colorMarkup.ts b/packages/cli-v3/src/utilities/colorMarkup.ts index 17c45eea3f..f827a5e661 100644 --- a/packages/cli-v3/src/utilities/colorMarkup.ts +++ b/packages/cli-v3/src/utilities/colorMarkup.ts @@ -26,10 +26,7 @@ type Token = { type: "text"; value: string } | { type: "styled"; tag: string; va * On malformed input (unclosed, mismatched, or nested tags), returns the entire * string styled with `fallbackStyle` (or unstyled if no fallback). */ -export function applyColorMarkup( - text: string, - fallbackStyle?: (t: string) => string -): string { +export function applyColorMarkup(text: string, fallbackStyle?: (t: string) => string): string { const tokens = tokenize(text); if (!tokens) { // Malformed markup β€” apply fallback to entire string diff --git a/packages/cli-v3/src/utilities/discoveryCheck.ts b/packages/cli-v3/src/utilities/discoveryCheck.ts index 8f0ceb46a6..632eaf7604 100644 --- a/packages/cli-v3/src/utilities/discoveryCheck.ts +++ b/packages/cli-v3/src/utilities/discoveryCheck.ts @@ -61,8 +61,7 @@ async function doEvaluate(spec: DiscoverySpec, projectRoot: string): Promise { +async function resolveFilePatterns(patterns: string[], projectRoot: string): Promise { const matched: string[] = []; for (const pattern of patterns) { @@ -120,10 +116,7 @@ async function resolveFilePatterns( return matched; } -async function checkContentPattern( - files: string[], - contentPattern: string -): Promise { +async function checkContentPattern(files: string[], contentPattern: string): Promise { const useFastPath = !REGEX_METACHARACTERS.test(contentPattern); // Pre-compile regex once outside the loop to avoid repeated compilation diff --git a/packages/cli-v3/src/utilities/platformNotifications.ts b/packages/cli-v3/src/utilities/platformNotifications.ts index cfdbc83ff4..8b04a66e27 100644 --- a/packages/cli-v3/src/utilities/platformNotifications.ts +++ b/packages/cli-v3/src/utilities/platformNotifications.ts @@ -27,10 +27,7 @@ export async function fetchPlatformNotification( options: FetchNotificationOptions ): Promise { const [error, result] = await tryCatch( - options.apiClient.getCliPlatformNotification( - options.projectRef, - AbortSignal.timeout(7000) - ) + options.apiClient.getCliPlatformNotification(options.projectRef, AbortSignal.timeout(7000)) ); if (error) { @@ -63,9 +60,7 @@ export async function fetchPlatformNotification( return { level: type, title, description, actionUrl }; } -function displayPlatformNotification( - notification: PlatformNotification | undefined -): void { +function displayPlatformNotification(notification: PlatformNotification | undefined): void { if (!notification) return; const message = formatNotificationMessage(notification); diff --git a/packages/core/src/schemas/eventFilter.ts b/packages/core/src/schemas/eventFilter.ts index 81be2e7c12..66b4d64546 100644 --- a/packages/core/src/schemas/eventFilter.ts +++ b/packages/core/src/schemas/eventFilter.ts @@ -53,8 +53,8 @@ const EventMatcherSchema = z.union([ $includes: z.union([z.string(), z.number(), z.boolean()]), }), z.object({ - $not: z.union([z.string(), z.number(), z.boolean()]) - }) + $not: z.union([z.string(), z.number(), z.boolean()]), + }), ]) ), ]); diff --git a/packages/core/src/v3/apiClient/core.ts b/packages/core/src/v3/apiClient/core.ts index 5541bb2a9a..5128aa50d4 100644 --- a/packages/core/src/v3/apiClient/core.ts +++ b/packages/core/src/v3/apiClient/core.ts @@ -603,14 +603,14 @@ async function waitForRetry( } // https://stackoverflow.com/a/34491287 -export function isEmptyObj(obj: Object | null | undefined): boolean { +export function isEmptyObj(obj: object | null | undefined): boolean { if (!obj) return true; for (const _k in obj) return false; return true; } // https://eslint.org/docs/latest/rules/no-prototype-builtins -export function hasOwn(obj: Object, key: string): boolean { +export function hasOwn(obj: object, key: string): boolean { return Object.prototype.hasOwnProperty.call(obj, key); } diff --git a/packages/core/src/v3/apiClient/errors.ts b/packages/core/src/v3/apiClient/errors.ts index 5f38a4947b..e49fe80759 100644 --- a/packages/core/src/v3/apiClient/errors.ts +++ b/packages/core/src/v3/apiClient/errors.ts @@ -3,7 +3,7 @@ export type APIHeaders = Record; export class ApiError extends Error { readonly status: number | undefined; readonly headers: APIHeaders | undefined; - readonly error: Object | undefined; + readonly error: object | undefined; readonly code: string | null | undefined; readonly param: string | null | undefined; @@ -11,7 +11,7 @@ export class ApiError extends Error { constructor( status: number | undefined, - error: Object | undefined, + error: object | undefined, message: string | undefined, headers: APIHeaders | undefined ) { @@ -33,10 +33,10 @@ export class ApiError extends Error { ? error.message : JSON.stringify(error.message) : typeof error === "string" - ? error - : error - ? JSON.stringify(error) - : undefined; + ? error + : error + ? JSON.stringify(error) + : undefined; if (errorMessage) { return errorMessage; @@ -59,7 +59,7 @@ export class ApiError extends Error { static generate( status: number | undefined, - errorResponse: Object | undefined, + errorResponse: object | undefined, message: string | undefined, headers: APIHeaders | undefined ) { @@ -117,15 +117,15 @@ export class ApiConnectionError extends ApiError { } export class BadRequestError extends ApiError { - override readonly status: 400 = 400; + override readonly status = 400 as const; } export class AuthenticationError extends ApiError { - override readonly status: 401 = 401; + override readonly status = 401 as const; } export class PermissionDeniedError extends ApiError { - override readonly status: 403 = 403; + override readonly status = 403 as const; } /** @@ -141,19 +141,19 @@ export function isTriggerRealtimeAuthError(error: unknown): boolean { } export class NotFoundError extends ApiError { - override readonly status: 404 = 404; + override readonly status = 404 as const; } export class ConflictError extends ApiError { - override readonly status: 409 = 409; + override readonly status = 409 as const; } export class UnprocessableEntityError extends ApiError { - override readonly status: 422 = 422; + override readonly status = 422 as const; } export class RateLimitError extends ApiError { - override readonly status: 429 = 429; + override readonly status = 429 as const; get millisecondsUntilReset(): number | undefined { // x-ratelimit-reset is the unix timestamp in milliseconds when the rate limit will reset. @@ -177,7 +177,7 @@ export class RateLimitError extends ApiError { export class InternalServerError extends ApiError {} export class ApiSchemaValidationError extends ApiError { - override readonly status: 200 = 200; + override readonly status = 200 as const; readonly rawBody: any; constructor({ diff --git a/packages/core/src/v3/apiClient/getBranch.ts b/packages/core/src/v3/apiClient/getBranch.ts index 1e1873fc2a..51ac8b1bc7 100644 --- a/packages/core/src/v3/apiClient/getBranch.ts +++ b/packages/core/src/v3/apiClient/getBranch.ts @@ -33,11 +33,7 @@ export function getBranch({ return undefined; } -export function getDevBranch({ - specified, -}: { - specified?: string; -}): string | undefined { +export function getDevBranch({ specified }: { specified?: string }): string | undefined { // For development we don't look at git/Vercel β€” only the flag and our env var. const branch = specified ?? getEnvVar("TRIGGER_DEV_BRANCH"); diff --git a/packages/core/src/v3/apiClient/index.ts b/packages/core/src/v3/apiClient/index.ts index e9e663223d..410bac0f17 100644 --- a/packages/core/src/v3/apiClient/index.ts +++ b/packages/core/src/v3/apiClient/index.ts @@ -118,10 +118,7 @@ import { RealtimeRunSkipColumns, type SSEStreamPart, } from "./runStream.js"; -import { - controlSubtype, - type ControlEvent, -} from "../sessionStreams/wireProtocol.js"; +import { controlSubtype, type ControlEvent } from "../sessionStreams/wireProtocol.js"; import { CreateEnvironmentVariableParams, ImportEnvironmentVariablesParams, @@ -497,9 +494,9 @@ export class ApiClient { await safeStreamCancel(forRetry); const errText = await response.text().catch((e) => (e as Error).message); - let errJSON: Object | undefined; + let errJSON: object | undefined; try { - errJSON = JSON.parse(errText) as Object; + errJSON = JSON.parse(errText) as object; } catch { // ignore } @@ -1868,9 +1865,7 @@ export class ApiClient { ); } - async listDashboards( - requestOptions?: ZodFetchOptions - ): Promise { + async listDashboards(requestOptions?: ZodFetchOptions): Promise { return zodfetch( ListDashboardsResponseBody, `${this.baseUrl}/api/v1/query/dashboards`, @@ -1949,11 +1944,7 @@ export class ApiClient { return headers; } - resolvePrompt( - slug: string, - body: ResolvePromptRequestBody, - requestOptions?: ZodFetchOptions - ) { + resolvePrompt(slug: string, body: ResolvePromptRequestBody, requestOptions?: ZodFetchOptions) { return zodfetch( ResolvePromptResponseBody, `${this.baseUrl}/api/v1/prompts/${slug}`, @@ -1984,7 +1975,11 @@ export class ApiClient { ); } - promotePromptVersion(slug: string, body: PromotePromptVersionRequestBody, requestOptions?: ZodFetchOptions) { + promotePromptVersion( + slug: string, + body: PromotePromptVersionRequestBody, + requestOptions?: ZodFetchOptions + ) { return zodfetch( PromptOkResponseBody, `${this.baseUrl}/api/v1/prompts/${slug}/promote`, @@ -1993,7 +1988,11 @@ export class ApiClient { ); } - createPromptOverride(slug: string, body: CreatePromptOverrideRequestBody, requestOptions?: ZodFetchOptions) { + createPromptOverride( + slug: string, + body: CreatePromptOverrideRequestBody, + requestOptions?: ZodFetchOptions + ) { return zodfetch( PromptOverrideCreatedResponseBody, `${this.baseUrl}/api/v1/prompts/${slug}/override`, @@ -2002,7 +2001,11 @@ export class ApiClient { ); } - updatePromptOverride(slug: string, body: UpdatePromptOverrideRequestBody, requestOptions?: ZodFetchOptions) { + updatePromptOverride( + slug: string, + body: UpdatePromptOverrideRequestBody, + requestOptions?: ZodFetchOptions + ) { return zodfetch( PromptOkResponseBody, `${this.baseUrl}/api/v1/prompts/${slug}/override`, @@ -2020,7 +2023,11 @@ export class ApiClient { ); } - reactivatePromptOverride(slug: string, body: ReactivatePromptOverrideRequestBody, requestOptions?: ZodFetchOptions) { + reactivatePromptOverride( + slug: string, + body: ReactivatePromptOverrideRequestBody, + requestOptions?: ZodFetchOptions + ) { return zodfetch( PromptOkResponseBody, `${this.baseUrl}/api/v1/prompts/${slug}/override/reactivate`, diff --git a/packages/core/src/v3/apiClient/runStream.test.ts b/packages/core/src/v3/apiClient/runStream.test.ts index 4ac2880976..0dca73779a 100644 --- a/packages/core/src/v3/apiClient/runStream.test.ts +++ b/packages/core/src/v3/apiClient/runStream.test.ts @@ -451,7 +451,11 @@ describe("SSEStreamSubscription v2 batch parsing β€” record kinds", () => { vi.restoreAllMocks(); }); - type ParsedPart = { id: string; chunk: unknown; headers?: ReadonlyArray }; + type ParsedPart = { + id: string; + chunk: unknown; + headers?: ReadonlyArray; + }; // Build a v2 batch SSE response with the given records and close. function makeBatchResponse( diff --git a/packages/core/src/v3/apiClient/runStream.ts b/packages/core/src/v3/apiClient/runStream.ts index 6d58dff1a7..d89b533d57 100644 --- a/packages/core/src/v3/apiClient/runStream.ts +++ b/packages/core/src/v3/apiClient/runStream.ts @@ -251,9 +251,7 @@ export class SSEStreamSubscription implements StreamSubscription { this.retryJitter = options.retryJitter ?? 0.5; this.fetchTimeoutMs = options.fetchTimeoutMs ?? 30_000; this.stallTimeoutMs = options.stallTimeoutMs ?? 0; - this.nonRetryableStatuses = new Set( - options.nonRetryableStatuses ?? [400, 404, 409, 410, 422] - ); + this.nonRetryableStatuses = new Set(options.nonRetryableStatuses ?? [400, 404, 409, 410, 422]); } /** diff --git a/packages/core/src/v3/apiClientManager/index.ts b/packages/core/src/v3/apiClientManager/index.ts index 0107e62249..74ff7ed15a 100644 --- a/packages/core/src/v3/apiClientManager/index.ts +++ b/packages/core/src/v3/apiClientManager/index.ts @@ -114,7 +114,13 @@ export class APIClientManagerAPI { const requestOptions = source?.requestOptions; const futureFlags = source?.future; - return new ApiClient(this.baseURL, this.accessToken, this.branchName, requestOptions, futureFlags); + return new ApiClient( + this.baseURL, + this.accessToken, + this.branchName, + requestOptions, + futureFlags + ); } clientOrThrow(config?: ApiClientConfiguration): ApiClient { diff --git a/packages/core/src/v3/auth/environment.ts b/packages/core/src/v3/auth/environment.ts index 8918f19130..498393722f 100644 --- a/packages/core/src/v3/auth/environment.ts +++ b/packages/core/src/v3/auth/environment.ts @@ -20,11 +20,7 @@ // them here keeps the contract structural (no @trigger.dev/database // import) while giving downstream consumers the same exact union they // expect when this value is passed to a Prisma column. -export type RuntimeEnvironmentType = - | "PRODUCTION" - | "STAGING" - | "DEVELOPMENT" - | "PREVIEW"; +export type RuntimeEnvironmentType = "PRODUCTION" | "STAGING" | "DEVELOPMENT" | "PREVIEW"; export type RunEngineVersion = "V1" | "V2"; diff --git a/packages/core/src/v3/errors.test.ts b/packages/core/src/v3/errors.test.ts index b02bbf5f3c..f5df741f17 100644 --- a/packages/core/src/v3/errors.test.ts +++ b/packages/core/src/v3/errors.test.ts @@ -12,9 +12,7 @@ describe("DuplicateTaskIdsError", () => { }); test("names the id and both files when an id is defined in two files", () => { - const error = new DuplicateTaskIdsError([ - { id: "foo", filePaths: ["src/a.ts", "src/b.ts"] }, - ]); + const error = new DuplicateTaskIdsError([{ id: "foo", filePaths: ["src/a.ts", "src/b.ts"] }]); expect(error.message).toContain('"foo"'); expect(error.message).toContain("src/a.ts"); diff --git a/packages/core/src/v3/errors.ts b/packages/core/src/v3/errors.ts index 187cbef9b6..7b396c0b90 100644 --- a/packages/core/src/v3/errors.ts +++ b/packages/core/src/v3/errors.ts @@ -339,13 +339,9 @@ export function sanitizeError(error: TaskRunError): TaskRunError { type: "INTERNAL_ERROR", code: error.code, message: - error.message != null - ? truncateMessage(error.message.replace(/\0/g, "")) - : undefined, + error.message != null ? truncateMessage(error.message.replace(/\0/g, "")) : undefined, stackTrace: - error.stackTrace != null - ? truncateStack(error.stackTrace.replace(/\0/g, "")) - : undefined, + error.stackTrace != null ? truncateStack(error.stackTrace.replace(/\0/g, "")) : undefined, }; } } diff --git a/packages/core/src/v3/idempotencyKeys.ts b/packages/core/src/v3/idempotencyKeys.ts index 3a6946ccbd..aafe9f906f 100644 --- a/packages/core/src/v3/idempotencyKeys.ts +++ b/packages/core/src/v3/idempotencyKeys.ts @@ -10,7 +10,10 @@ import { digestSHA256 } from "./utils/crypto.js"; import type { ZodFetchOptions } from "./apiClient/core.js"; // Re-export types from catalog for backwards compatibility -export type { IdempotencyKeyScope, IdempotencyKeyOptions } from "./idempotency-key-catalog/catalog.js"; +export type { + IdempotencyKeyScope, + IdempotencyKeyOptions, +} from "./idempotency-key-catalog/catalog.js"; /** * Extracts the user-provided key and scope from an idempotency key created with `idempotencyKeys.create()`. @@ -227,9 +230,7 @@ export async function resetIdempotencyKey( // Try to extract options from an IdempotencyKey created with idempotencyKeys.create() const attachedOptions = - typeof idempotencyKey === "string" - ? getIdempotencyKeyOptions(idempotencyKey) - : undefined; + typeof idempotencyKey === "string" ? getIdempotencyKeyOptions(idempotencyKey) : undefined; const scope = attachedOptions?.scope ?? options?.scope ?? "run"; const keyArray = Array.isArray(idempotencyKey) diff --git a/packages/core/src/v3/inputStreams/manager.ts b/packages/core/src/v3/inputStreams/manager.ts index 51424df39f..11f9f497b2 100644 --- a/packages/core/src/v3/inputStreams/manager.ts +++ b/packages/core/src/v3/inputStreams/manager.ts @@ -23,7 +23,6 @@ type OnceWaiter = { abortHandler?: () => void; }; - type TailState = { abortController: AbortController; promise: Promise; @@ -198,7 +197,8 @@ export class StandardInputStreamManager implements InputStreamManager { // Abort tails that no longer have any once waiters either for (const [streamId, tail] of this.tails) { - const hasWaiters = this.onceWaiters.has(streamId) && this.onceWaiters.get(streamId)!.length > 0; + const hasWaiters = + this.onceWaiters.has(streamId) && this.onceWaiters.get(streamId)!.length > 0; if (!hasWaiters) { tail.abortController.abort(); this.tails.delete(streamId); @@ -304,8 +304,7 @@ export class StandardInputStreamManager implements InputStreamManager { // failure (auth rejected, 5xx, DNS) would reconnect in a tight // loop because `#runTail`'s error path only logs. `#dispatch` // resets the counter on every successful record. - const hasHandlers = - this.handlers.has(streamId) && this.handlers.get(streamId)!.size > 0; + const hasHandlers = this.handlers.has(streamId) && this.handlers.get(streamId)!.size > 0; const hasWaiters = this.onceWaiters.has(streamId) && this.onceWaiters.get(streamId)!.length > 0; if (hasHandlers || hasWaiters) { @@ -318,8 +317,7 @@ export class StandardInputStreamManager implements InputStreamManager { const stillHasHandlers = this.handlers.has(streamId) && this.handlers.get(streamId)!.size > 0; const stillHasWaiters = - this.onceWaiters.has(streamId) && - this.onceWaiters.get(streamId)!.length > 0; + this.onceWaiters.has(streamId) && this.onceWaiters.get(streamId)!.length > 0; if (!stillHasHandlers && !stillHasWaiters) return; this.#ensureStreamTailConnected(streamId); }, delayMs); @@ -332,34 +330,30 @@ export class StandardInputStreamManager implements InputStreamManager { async #runTail(runId: string, streamId: string, signal: AbortSignal): Promise { try { const lastSeq = this.seqNums.get(streamId); - const stream = await this.apiClient.fetchStream( - runId, - `input/${streamId}`, - { - signal, - baseUrl: this.baseUrl, - // Max allowed by the SSE endpoint is 600s; the tail will reconnect on close - timeoutInSeconds: 600, - // Resume from last seen sequence number to avoid replaying history on reconnect - lastEventId: lastSeq !== undefined ? String(lastSeq) : undefined, - onPart: (part) => { - const seqNum = parseInt(part.id, 10); - if (Number.isFinite(seqNum)) { - this.seqNums.set(streamId, seqNum); - } - }, - onComplete: () => { - if (this.debug) { - console.log(`[InputStreamManager] Tail stream completed for "${streamId}"`); - } - }, - onError: (error) => { - if (this.debug) { - console.error(`[InputStreamManager] Tail stream error for "${streamId}":`, error); - } - }, - } - ); + const stream = await this.apiClient.fetchStream(runId, `input/${streamId}`, { + signal, + baseUrl: this.baseUrl, + // Max allowed by the SSE endpoint is 600s; the tail will reconnect on close + timeoutInSeconds: 600, + // Resume from last seen sequence number to avoid replaying history on reconnect + lastEventId: lastSeq !== undefined ? String(lastSeq) : undefined, + onPart: (part) => { + const seqNum = parseInt(part.id, 10); + if (Number.isFinite(seqNum)) { + this.seqNums.set(streamId, seqNum); + } + }, + onComplete: () => { + if (this.debug) { + console.log(`[InputStreamManager] Tail stream completed for "${streamId}"`); + } + }, + onError: (error) => { + if (this.debug) { + console.error(`[InputStreamManager] Tail stream error for "${streamId}":`, error); + } + }, + }); for await (const record of stream) { if (signal.aborted) break; diff --git a/packages/core/src/v3/inputStreams/noopManager.ts b/packages/core/src/v3/inputStreams/noopManager.ts index 612da832d7..5aca9c8ed9 100644 --- a/packages/core/src/v3/inputStreams/noopManager.ts +++ b/packages/core/src/v3/inputStreams/noopManager.ts @@ -24,7 +24,9 @@ export class NoopInputStreamManager implements InputStreamManager { setLastSeqNum(_streamId: string, _seqNum: number): void {} - shiftBuffer(_streamId: string): boolean { return false; } + shiftBuffer(_streamId: string): boolean { + return false; + } disconnectStream(_streamId: string): void {} diff --git a/packages/core/src/v3/otel/nodejsRuntimeMetrics.ts b/packages/core/src/v3/otel/nodejsRuntimeMetrics.ts index aeb4ce4bee..aa24fddfa0 100644 --- a/packages/core/src/v3/otel/nodejsRuntimeMetrics.ts +++ b/packages/core/src/v3/otel/nodejsRuntimeMetrics.ts @@ -56,26 +56,23 @@ export function startNodejsRuntimeMetrics(meterProvider: MeterProvider) { observables.push(heapUsed, heapTotal); // Single batch callback for all metrics - meter.addBatchObservableCallback( - (obs) => { - // ELU - const currentElu = performance.eventLoopUtilization(); - const diff = performance.eventLoopUtilization(currentElu, lastElu); - lastElu = currentElu; - obs.observe(eluGauge, diff.utilization); + meter.addBatchObservableCallback((obs) => { + // ELU + const currentElu = performance.eventLoopUtilization(); + const diff = performance.eventLoopUtilization(currentElu, lastElu); + lastElu = currentElu; + obs.observe(eluGauge, diff.utilization); - // Event loop delay (nanoseconds -> seconds) - if (eld && eldP95 && eldMax) { - obs.observe(eldP95, eld.percentile(95) / 1e9); - obs.observe(eldMax, eld.max / 1e9); - eld.reset(); - } + // Event loop delay (nanoseconds -> seconds) + if (eld && eldP95 && eldMax) { + obs.observe(eldP95, eld.percentile(95) / 1e9); + obs.observe(eldMax, eld.max / 1e9); + eld.reset(); + } - // Heap - const mem = process.memoryUsage(); - obs.observe(heapUsed, mem.heapUsed); - obs.observe(heapTotal, mem.heapTotal); - }, - observables - ); + // Heap + const mem = process.memoryUsage(); + obs.observe(heapUsed, mem.heapUsed); + obs.observe(heapTotal, mem.heapTotal); + }, observables); } diff --git a/packages/core/src/v3/otel/tracingSDK.ts b/packages/core/src/v3/otel/tracingSDK.ts index f03bd2fc3a..0dd56f5a6f 100644 --- a/packages/core/src/v3/otel/tracingSDK.ts +++ b/packages/core/src/v3/otel/tracingSDK.ts @@ -175,24 +175,19 @@ export class TracingSDK { for (const exporter of config.exporters ?? []) { spanProcessors.push( getEnvVar("TRIGGER_OTEL_BATCH_PROCESSING_ENABLED") === "1" - ? new BatchSpanProcessor( - new ExternalSpanExporterWrapper(exporter, externalTraceId), - { - maxExportBatchSize: parseInt( - getEnvVar("TRIGGER_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE") ?? "64" - ), - scheduledDelayMillis: parseInt( - getEnvVar("TRIGGER_OTEL_SPAN_SCHEDULED_DELAY_MILLIS") ?? "200" - ), - exportTimeoutMillis: parseInt( - getEnvVar("TRIGGER_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS") ?? "30000" - ), - maxQueueSize: parseInt(getEnvVar("TRIGGER_OTEL_SPAN_MAX_QUEUE_SIZE") ?? "512"), - } - ) - : new SimpleSpanProcessor( - new ExternalSpanExporterWrapper(exporter, externalTraceId) - ) + ? new BatchSpanProcessor(new ExternalSpanExporterWrapper(exporter, externalTraceId), { + maxExportBatchSize: parseInt( + getEnvVar("TRIGGER_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE") ?? "64" + ), + scheduledDelayMillis: parseInt( + getEnvVar("TRIGGER_OTEL_SPAN_SCHEDULED_DELAY_MILLIS") ?? "200" + ), + exportTimeoutMillis: parseInt( + getEnvVar("TRIGGER_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS") ?? "30000" + ), + maxQueueSize: parseInt(getEnvVar("TRIGGER_OTEL_SPAN_MAX_QUEUE_SIZE") ?? "512"), + }) + : new SimpleSpanProcessor(new ExternalSpanExporterWrapper(exporter, externalTraceId)) ); } @@ -282,9 +277,7 @@ export class TracingSDK { // Metrics setup const metricsUrl = - config.metricsUrl ?? - getEnvVar("TRIGGER_OTEL_METRICS_ENDPOINT") ?? - `${config.url}/v1/metrics`; + config.metricsUrl ?? getEnvVar("TRIGGER_OTEL_METRICS_ENDPOINT") ?? `${config.url}/v1/metrics`; const rawMetricExporter = new OTLPMetricExporter({ url: metricsUrl, @@ -511,9 +504,7 @@ class ExternalLogRecordExporterWrapper { return; } - const modifiedLogs = logs.map((log) => - this.transformLogRecord(log, externalTraceContext) - ); + const modifiedLogs = logs.map((log) => this.transformLogRecord(log, externalTraceContext)); this.underlyingExporter.export(modifiedLogs, resultCallback); } @@ -527,9 +518,7 @@ class ExternalLogRecordExporterWrapper { forceFlush?: () => Promise; }; - return underlyingExporter.forceFlush - ? underlyingExporter.forceFlush() - : Promise.resolve(); + return underlyingExporter.forceFlush ? underlyingExporter.forceFlush() : Promise.resolve(); } transformLogRecord( diff --git a/packages/core/src/v3/realtimeStreams/manager.test.ts b/packages/core/src/v3/realtimeStreams/manager.test.ts index 179754bc75..93268953c5 100644 --- a/packages/core/src/v3/realtimeStreams/manager.test.ts +++ b/packages/core/src/v3/realtimeStreams/manager.test.ts @@ -58,10 +58,7 @@ describe("StandardRealtimeStreamsManager createStream cache", () => { const { client, spy } = makeApiClient(async () => ({ version: "v1", headers: {} })); const manager = new StandardRealtimeStreamsManager(client, "http://localhost"); - await Promise.all([ - getCached(manager, "run-1", "chat"), - getCached(manager, "run-2", "chat"), - ]); + await Promise.all([getCached(manager, "run-1", "chat"), getCached(manager, "run-2", "chat")]); expect(spy).toHaveBeenCalledTimes(2); }); @@ -108,11 +105,7 @@ describe("StandardRealtimeStreamsManager createStream cache", () => { // writer's `wait()` rejects. ( manager as unknown as { - evictCreateStreamIfStale: ( - runId: string, - key: string, - expected: Promise - ) => void; + evictCreateStreamIfStale: (runId: string, key: string, expected: Promise) => void; } ).evictCreateStreamIfStale("run-1", "chat", cachedPromise); @@ -132,11 +125,7 @@ describe("StandardRealtimeStreamsManager createStream cache", () => { const stalePromise = Promise.resolve({ version: "v1", headers: {} }); ( manager as unknown as { - evictCreateStreamIfStale: ( - runId: string, - key: string, - expected: Promise - ) => void; + evictCreateStreamIfStale: (runId: string, key: string, expected: Promise) => void; } ).evictCreateStreamIfStale("run-1", "chat", stalePromise); diff --git a/packages/core/src/v3/realtimeStreams/manager.ts b/packages/core/src/v3/realtimeStreams/manager.ts index f4d915acc3..b72ce8af02 100644 --- a/packages/core/src/v3/realtimeStreams/manager.ts +++ b/packages/core/src/v3/realtimeStreams/manager.ts @@ -98,17 +98,13 @@ export class StandardRealtimeStreamsManager implements RealtimeStreamsManager { const abortController = new AbortController(); // Chain with user-provided signal if present const combinedSignal = options?.signal - ? AbortSignal.any?.([options.signal, abortController.signal]) ?? abortController.signal + ? (AbortSignal.any?.([options.signal, abortController.signal]) ?? abortController.signal) : abortController.signal; // Capture which cached promise this writer uses so reactive // invalidation below evicts only if the cache still holds it (a // concurrent caller may have already refreshed it). - const activeCreatePromise = this.getCachedCreateStream( - runId, - key, - options?.requestOptions - ); + const activeCreatePromise = this.getCachedCreateStream(runId, key, options?.requestOptions); const streamInstance = new StreamInstance({ apiClient: this.apiClient, diff --git a/packages/core/src/v3/realtimeStreams/streamsWriterV1.ts b/packages/core/src/v3/realtimeStreams/streamsWriterV1.ts index c19faf6c2f..3b51f8540b 100644 --- a/packages/core/src/v3/realtimeStreams/streamsWriterV1.ts +++ b/packages/core/src/v3/realtimeStreams/streamsWriterV1.ts @@ -433,7 +433,7 @@ export class StreamsWriterV1 implements StreamsWriter { const lastChunkHeader = res.headers["x-last-chunk-index"]; if (lastChunkHeader) { const lastChunkIndex = parseInt( - Array.isArray(lastChunkHeader) ? lastChunkHeader[0] ?? "0" : lastChunkHeader ?? "0", + Array.isArray(lastChunkHeader) ? (lastChunkHeader[0] ?? "0") : (lastChunkHeader ?? "0"), 10 ); resolve(lastChunkIndex); diff --git a/packages/core/src/v3/realtimeStreams/types.ts b/packages/core/src/v3/realtimeStreams/types.ts index 5e537d991f..87614ff062 100644 --- a/packages/core/src/v3/realtimeStreams/types.ts +++ b/packages/core/src/v3/realtimeStreams/types.ts @@ -200,7 +200,9 @@ export type RealtimeDefinedInputStream = { * then suspends via `.wait()` if no data arrives. If data arrives during * the idle phase the task responds instantly without suspending. */ - waitWithIdleTimeout: (options: InputStreamWaitWithIdleTimeoutOptions) => Promise<{ ok: true; output: TData } | { ok: false; error?: any }>; + waitWithIdleTimeout: ( + options: InputStreamWaitWithIdleTimeoutOptions + ) => Promise<{ ok: true; output: TData } | { ok: false; error?: any }>; /** * Send data to this input stream on a specific run. * This is used from outside the task (e.g., from your backend or another task). @@ -272,9 +274,8 @@ export type InputStreamWaitWithIdleTimeoutOptions = { skipSuspend?: boolean; }; -export type InferInputStreamType = T extends RealtimeDefinedInputStream - ? TData - : unknown; +export type InferInputStreamType = + T extends RealtimeDefinedInputStream ? TData : unknown; /** * Internal record format for multiplexed input stream data on S2. diff --git a/packages/core/src/v3/resource-catalog/catalog.ts b/packages/core/src/v3/resource-catalog/catalog.ts index 1036152c92..3c12871696 100644 --- a/packages/core/src/v3/resource-catalog/catalog.ts +++ b/packages/core/src/v3/resource-catalog/catalog.ts @@ -6,7 +6,11 @@ import { TaskManifest, WorkerManifest, } from "../schemas/index.js"; -import { PromptMetadataWithFunctions, TaskMetadataWithFunctions, TaskSchema } from "../types/index.js"; +import { + PromptMetadataWithFunctions, + TaskMetadataWithFunctions, + TaskSchema, +} from "../types/index.js"; export interface ResourceCatalog { setCurrentFileContext(filePath: string, entryPoint: string): void; diff --git a/packages/core/src/v3/resource-catalog/index.ts b/packages/core/src/v3/resource-catalog/index.ts index 4e6e278ad0..8ef04516f8 100644 --- a/packages/core/src/v3/resource-catalog/index.ts +++ b/packages/core/src/v3/resource-catalog/index.ts @@ -8,7 +8,11 @@ import { TaskManifest, WorkerManifest, } from "../schemas/index.js"; -import { PromptMetadataWithFunctions, TaskMetadataWithFunctions, TaskSchema } from "../types/index.js"; +import { + PromptMetadataWithFunctions, + TaskMetadataWithFunctions, + TaskSchema, +} from "../types/index.js"; import { getGlobal, registerGlobal, unregisterGlobal } from "../utils/globals.js"; import { type ResourceCatalog } from "./catalog.js"; import { NoopResourceCatalog } from "./noopResourceCatalog.js"; diff --git a/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts b/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts index 509b76c179..4697dc8c25 100644 --- a/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts +++ b/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts @@ -6,7 +6,11 @@ import { TaskManifest, WorkerManifest, } from "../schemas/index.js"; -import { type PromptMetadataWithFunctions, type TaskMetadataWithFunctions, type TaskSchema } from "../types/index.js"; +import { + type PromptMetadataWithFunctions, + type TaskMetadataWithFunctions, + type TaskSchema, +} from "../types/index.js"; import { ResourceCatalog } from "./catalog.js"; export class NoopResourceCatalog implements ResourceCatalog { diff --git a/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts b/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts index bf200936d6..8446144af9 100644 --- a/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts +++ b/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts @@ -9,7 +9,11 @@ import { WorkerManifest, QueueManifest, } from "../schemas/index.js"; -import { PromptMetadataWithFunctions, TaskMetadataWithFunctions, TaskSchema } from "../types/index.js"; +import { + PromptMetadataWithFunctions, + TaskMetadataWithFunctions, + TaskSchema, +} from "../types/index.js"; import { ResourceCatalog } from "./catalog.js"; /** @@ -118,10 +122,7 @@ export class StandardResourceCatalog implements ResourceCatalog { // registration. Skip the runtime sentinel context (a task() firing during // another task's run) β€” that's a re-registration, not a duplicate // definition, and the indexer never uses the sentinel. - if ( - this._taskMetadata.has(task.id) && - this._currentFileContext.filePath !== NO_FILE_CONTEXT - ) { + if (this._taskMetadata.has(task.id) && this._currentFileContext.filePath !== NO_FILE_CONTEXT) { const existingFilePath = this._taskFileMetadata.get(task.id)?.filePath; const currentFilePath = this._currentFileContext.filePath; const collision = this._taskIdCollisions.find((c) => c.id === task.id); diff --git a/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.test.ts b/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.test.ts index bbf93ad96f..f19c18aa8c 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.test.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.test.ts @@ -66,7 +66,9 @@ describe("RunQueueConsumer dequeue latency metric", () => { const text = await register.metrics(); // One observation for the whole batch, not one per message. - expect(text).toContain('queue_consumer_pool_dequeue_duration_seconds_count{outcome="success"} 1'); + expect(text).toContain( + 'queue_consumer_pool_dequeue_duration_seconds_count{outcome="success"} 1' + ); }); it('records outcome="error" when the response is unsuccessful', async () => { @@ -99,6 +101,8 @@ describe("RunQueueConsumer dequeue latency metric", () => { ).resolves.not.toThrow(); // Histogram has no observations - the labelled count line should be absent. - expect(await register.metrics()).not.toContain("queue_consumer_pool_dequeue_duration_seconds_count"); + expect(await register.metrics()).not.toContain( + "queue_consumer_pool_dequeue_duration_seconds_count" + ); }); }); diff --git a/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.ts b/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.ts index cf48ce2e9e..91e05a1654 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.ts @@ -18,7 +18,10 @@ export type RunQueueConsumerOptions = { maxRunCount?: number; /** Which worker-queue class this consumer pulls from. Defaults to the worker's region queue. */ queueClass?: WorkerQueueClass; - onDequeue: (messages: WorkerApiDequeueResponseBody, timing?: { dequeueResponseMs: number; pollingIntervalMs: number }) => Promise; + onDequeue: ( + messages: WorkerApiDequeueResponseBody, + timing?: { dequeueResponseMs: number; pollingIntervalMs: number } + ) => Promise; /** Optional shared pool metrics. When provided, dequeue API latency is recorded as a histogram. */ metrics?: ConsumerPoolMetrics; }; @@ -29,7 +32,10 @@ export class RunQueueConsumer implements QueueConsumer { private readonly preSkip?: PreSkipFn; private readonly maxRunCount?: number; private readonly queueClass?: WorkerQueueClass; - private readonly onDequeue: (messages: WorkerApiDequeueResponseBody, timing?: { dequeueResponseMs: number; pollingIntervalMs: number }) => Promise; + private readonly onDequeue: ( + messages: WorkerApiDequeueResponseBody, + timing?: { dequeueResponseMs: number; pollingIntervalMs: number } + ) => Promise; private readonly metrics?: ConsumerPoolMetrics; private readonly logger = new SimpleStructuredLogger("queue-consumer"); @@ -142,7 +148,10 @@ export class RunQueueConsumer implements QueueConsumer { ); try { - await this.onDequeue(response.data, { dequeueResponseMs, pollingIntervalMs: this.lastScheduledIntervalMs }); + await this.onDequeue(response.data, { + dequeueResponseMs, + pollingIntervalMs: this.lastScheduledIntervalMs, + }); if (response.data.length > 0) { nextIntervalMs = this.intervalMs; diff --git a/packages/core/src/v3/runEngineWorker/supervisor/session.ts b/packages/core/src/v3/runEngineWorker/supervisor/session.ts index de823d792b..10e70994a0 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/session.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/session.ts @@ -87,7 +87,10 @@ export class SupervisorSession extends EventEmitter { }); } - private async onDequeue(messages: WorkerApiDequeueResponseBody, timing?: { dequeueResponseMs: number; pollingIntervalMs: number }): Promise { + private async onDequeue( + messages: WorkerApiDequeueResponseBody, + timing?: { dequeueResponseMs: number; pollingIntervalMs: number } + ): Promise { this.logger.verbose("Dequeued messages with contents", { count: messages.length, messages }); for (const message of messages) { diff --git a/packages/core/src/v3/runMetadata/noopManager.ts b/packages/core/src/v3/runMetadata/noopManager.ts index 28366b066b..1573dcce53 100644 --- a/packages/core/src/v3/runMetadata/noopManager.ts +++ b/packages/core/src/v3/runMetadata/noopManager.ts @@ -48,7 +48,7 @@ export class NoopRunMetadataManager implements RunMetadataManager { get parent(): RunMetadataUpdater { // Store a reference to this object const self = this; - + // Create a local reference to ensure proper context const parentUpdater: RunMetadataUpdater = { append: () => parentUpdater, @@ -65,14 +65,14 @@ export class NoopRunMetadataManager implements RunMetadataManager { }), update: () => parentUpdater, }; - + return parentUpdater; } get root(): RunMetadataUpdater { // Store a reference to this object const self = this; - + // Create a local reference to ensure proper context const rootUpdater: RunMetadataUpdater = { append: () => rootUpdater, @@ -89,7 +89,7 @@ export class NoopRunMetadataManager implements RunMetadataManager { }), update: () => rootUpdater, }; - + return rootUpdater; } } diff --git a/packages/core/src/v3/schemas/build.ts b/packages/core/src/v3/schemas/build.ts index e1543529a4..52a353aaf6 100644 --- a/packages/core/src/v3/schemas/build.ts +++ b/packages/core/src/v3/schemas/build.ts @@ -1,12 +1,6 @@ import { z } from "zod"; import { ConfigManifest } from "./config.js"; -import { - PromptManifest, - QueueManifest, - SkillManifest, - TaskFile, - TaskManifest, -} from "./schemas.js"; +import { PromptManifest, QueueManifest, SkillManifest, TaskFile, TaskManifest } from "./schemas.js"; export const BuildExternal = z.object({ name: z.string(), diff --git a/packages/core/src/v3/serverOnly/httpServer.ts b/packages/core/src/v3/serverOnly/httpServer.ts index 1dcddf8f15..d0cd9275f8 100644 --- a/packages/core/src/v3/serverOnly/httpServer.ts +++ b/packages/core/src/v3/serverOnly/httpServer.ts @@ -160,8 +160,14 @@ export class HttpServer { return reply.empty(405); } - const { handler, paramsSchema, querySchema, bodySchema, keepConnectionAlive, skipBodyParsing } = - routeDefinition; + const { + handler, + paramsSchema, + querySchema, + bodySchema, + keepConnectionAlive, + skipBodyParsing, + } = routeDefinition; const params = this.parseRouteParams(route, url); const parsedParams = this.optionalSchema(paramsSchema, params); diff --git a/packages/core/src/v3/serverOnly/idempotencyKeys.ts b/packages/core/src/v3/serverOnly/idempotencyKeys.ts index 91dac04fd4..c2a6a46990 100644 --- a/packages/core/src/v3/serverOnly/idempotencyKeys.ts +++ b/packages/core/src/v3/serverOnly/idempotencyKeys.ts @@ -37,7 +37,10 @@ export function extractIdempotencyKeyScope(run: { export function unsafeExtractIdempotencyKeyScope(run: { idempotencyKeyOptions: unknown; }): "run" | "attempt" | "global" | undefined { - const unsafe = run.idempotencyKeyOptions as { scope?: "run" | "attempt" | "global" } | undefined | null; + const unsafe = run.idempotencyKeyOptions as + | { scope?: "run" | "attempt" | "global" } + | undefined + | null; return unsafe?.scope ?? undefined; } @@ -65,5 +68,3 @@ export function unsafeExtractIdempotencyKeyUser(run: { return unsafe?.key ?? undefined; } - - diff --git a/packages/core/src/v3/sessionStreams/index.ts b/packages/core/src/v3/sessionStreams/index.ts index c150f0fe9d..b4e2e2eb29 100644 --- a/packages/core/src/v3/sessionStreams/index.ts +++ b/packages/core/src/v3/sessionStreams/index.ts @@ -1,10 +1,6 @@ import { getGlobal, registerGlobal } from "../utils/globals.js"; import { NoopSessionStreamManager } from "./noopManager.js"; -import { - InputStreamOncePromise, - SessionChannelIO, - SessionStreamManager, -} from "./types.js"; +import { InputStreamOncePromise, SessionChannelIO, SessionStreamManager } from "./types.js"; import { InputStreamOnceOptions } from "../realtimeStreams/types.js"; const API_NAME = "session-streams"; @@ -63,11 +59,7 @@ export class SessionStreamsAPI implements SessionStreamManager { return this.#getManager().lastDispatchedSeqNum(sessionId, io); } - public setLastDispatchedSeqNum( - sessionId: string, - io: SessionChannelIO, - seqNum: number - ): void { + public setLastDispatchedSeqNum(sessionId: string, io: SessionChannelIO, seqNum: number): void { this.#getManager().setLastDispatchedSeqNum(sessionId, io, seqNum); } diff --git a/packages/core/src/v3/sessionStreams/manager.test.ts b/packages/core/src/v3/sessionStreams/manager.test.ts index 6089d70578..e01bd5fff8 100644 --- a/packages/core/src/v3/sessionStreams/manager.test.ts +++ b/packages/core/src/v3/sessionStreams/manager.test.ts @@ -52,7 +52,10 @@ describe("StandardSessionStreamManager β€” minTimestamp filter", () => { { id: "0", chunk: { kind: "message", payload: { id: "u1" } }, timestamp: 1000 }, { id: "1", chunk: { kind: "message", payload: { id: "u2" } }, timestamp: 2000 }, ]; - const manager = new StandardSessionStreamManager(singleShotApiClient(records), "http://localhost"); + const manager = new StandardSessionStreamManager( + singleShotApiClient(records), + "http://localhost" + ); const first = await manager.once(sessionId, io); expect(first).toEqual({ ok: true, output: { kind: "message", payload: { id: "u1" } } }); @@ -70,7 +73,10 @@ describe("StandardSessionStreamManager β€” minTimestamp filter", () => { { id: "1", chunk: { kind: "message", payload: { id: "u2" } }, timestamp: 2000 }, { id: "2", chunk: { kind: "message", payload: { id: "u3" } }, timestamp: 3000 }, ]; - const manager = new StandardSessionStreamManager(singleShotApiClient(records), "http://localhost"); + const manager = new StandardSessionStreamManager( + singleShotApiClient(records), + "http://localhost" + ); // Cutoff at 2000 (inclusive: `<=` is dropped). Only u3 should pass. manager.setMinTimestamp(sessionId, io, 2000); @@ -86,7 +92,10 @@ describe("StandardSessionStreamManager β€” minTimestamp filter", () => { const records = [ { id: "0", chunk: { kind: "message", payload: { id: "u1" } }, timestamp: 1000 }, ]; - const manager = new StandardSessionStreamManager(singleShotApiClient(records), "http://localhost"); + const manager = new StandardSessionStreamManager( + singleShotApiClient(records), + "http://localhost" + ); manager.setMinTimestamp(sessionId, io, 5000); manager.setMinTimestamp(sessionId, io, undefined); @@ -99,9 +108,7 @@ describe("StandardSessionStreamManager β€” minTimestamp filter", () => { }); it("filter is per-(sessionId, io) and doesn't bleed across streams", async () => { - const inApi = singleShotApiClient([ - { id: "0", chunk: { kind: "in-record" }, timestamp: 1000 }, - ]); + const inApi = singleShotApiClient([{ id: "0", chunk: { kind: "in-record" }, timestamp: 1000 }]); const manager = new StandardSessionStreamManager(inApi, "http://localhost"); manager.setMinTimestamp(sessionId, "in", 5000); @@ -137,7 +144,10 @@ describe("StandardSessionStreamManager β€” minTimestamp filter", () => { const records = [ { id: "0", chunk: { kind: "message", payload: { id: "u1" } }, timestamp: 1000 }, ]; - const manager = new StandardSessionStreamManager(singleShotApiClient(records), "http://localhost"); + const manager = new StandardSessionStreamManager( + singleShotApiClient(records), + "http://localhost" + ); manager.setMinTimestamp(sessionId, io, 5000); manager.reset(); diff --git a/packages/core/src/v3/sessionStreams/manager.ts b/packages/core/src/v3/sessionStreams/manager.ts index 65eaf40f9c..1c9af1c7d6 100644 --- a/packages/core/src/v3/sessionStreams/manager.ts +++ b/packages/core/src/v3/sessionStreams/manager.ts @@ -98,11 +98,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { private debug: boolean = false ) {} - on( - sessionId: string, - io: SessionChannelIO, - handler: SessionStreamHandler - ): { off: () => void } { + on(sessionId: string, io: SessionChannelIO, handler: SessionStreamHandler): { off: () => void } { const key = keyFor(sessionId, io); let handlerSet = this.handlers.get(key); @@ -250,11 +246,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { return this.lastDispatchedSeqNums.get(keyFor(sessionId, io)); } - setLastDispatchedSeqNum( - sessionId: string, - io: SessionChannelIO, - seqNum: number - ): void { + setLastDispatchedSeqNum(sessionId: string, io: SessionChannelIO, seqNum: number): void { this.#advanceLastDispatched(keyFor(sessionId, io), seqNum); } @@ -265,11 +257,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { } } - setMinTimestamp( - sessionId: string, - io: SessionChannelIO, - minTimestamp: number | undefined - ): void { + setMinTimestamp(sessionId: string, io: SessionChannelIO, minTimestamp: number | undefined): void { const key = keyFor(sessionId, io); if (minTimestamp === undefined) { this.minTimestamps.delete(key); @@ -397,8 +385,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { } const hasHandlers = this.handlers.has(key) && this.handlers.get(key)!.size > 0; - const hasWaiters = - this.onceWaiters.has(key) && this.onceWaiters.get(key)!.length > 0; + const hasWaiters = this.onceWaiters.has(key) && this.onceWaiters.get(key)!.length > 0; if (hasHandlers || hasWaiters) { // Exponential backoff with jitter. 1s base, doubling each // attempt, capped at 30s. Without this, a persistent backend @@ -415,8 +402,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { // handlers/waiters means we should stay quiet. if (this.tails.has(key)) return; if (this.explicitlyDisconnected.has(key)) return; - const stillHasHandlers = - this.handlers.has(key) && this.handlers.get(key)!.size > 0; + const stillHasHandlers = this.handlers.has(key) && this.handlers.get(key)!.size > 0; const stillHasWaiters = this.onceWaiters.has(key) && this.onceWaiters.get(key)!.length > 0; if (!stillHasHandlers && !stillHasWaiters) return; @@ -427,11 +413,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { this.tails.set(key, { abortController, promise }); } - async #runTail( - sessionId: string, - io: SessionChannelIO, - signal: AbortSignal - ): Promise { + async #runTail(sessionId: string, io: SessionChannelIO, signal: AbortSignal): Promise { const key = keyFor(sessionId, io); try { const lastSeq = this.seqNums.get(key); diff --git a/packages/core/src/v3/sessionStreams/noopManager.ts b/packages/core/src/v3/sessionStreams/noopManager.ts index 77d1f40ac9..5284c9a9fe 100644 --- a/packages/core/src/v3/sessionStreams/noopManager.ts +++ b/packages/core/src/v3/sessionStreams/noopManager.ts @@ -35,11 +35,7 @@ export class NoopSessionStreamManager implements SessionStreamManager { return undefined; } - setLastDispatchedSeqNum( - _sessionId: string, - _io: SessionChannelIO, - _seqNum: number - ): void {} + setLastDispatchedSeqNum(_sessionId: string, _io: SessionChannelIO, _seqNum: number): void {} setMinTimestamp( _sessionId: string, diff --git a/packages/core/src/v3/sessionStreams/types.ts b/packages/core/src/v3/sessionStreams/types.ts index 45be5149e2..bd98fa0438 100644 --- a/packages/core/src/v3/sessionStreams/types.ts +++ b/packages/core/src/v3/sessionStreams/types.ts @@ -70,11 +70,7 @@ export interface SessionStreamManager { * `session-in-event-id` header on the latest `turn-complete` on * `.out`. Monotonic: only ever advances forward, never backwards. */ - setLastDispatchedSeqNum( - sessionId: string, - io: SessionChannelIO, - seqNum: number - ): void; + setLastDispatchedSeqNum(sessionId: string, io: SessionChannelIO, seqNum: number): void; /** * Set a per-stream lower-bound SSE timestamp. Records whose timestamp @@ -84,11 +80,7 @@ export interface SessionStreamManager { * * Pass `undefined` to clear the filter. */ - setMinTimestamp( - sessionId: string, - io: SessionChannelIO, - minTimestamp: number | undefined - ): void; + setMinTimestamp(sessionId: string, io: SessionChannelIO, minTimestamp: number | undefined): void; /** Remove and discard the first buffered record. Returns true if one was removed. */ shiftBuffer(sessionId: string, io: SessionChannelIO): boolean; diff --git a/packages/core/src/v3/taskContext/otelProcessors.ts b/packages/core/src/v3/taskContext/otelProcessors.ts index fc30e9d114..452ecf391a 100644 --- a/packages/core/src/v3/taskContext/otelProcessors.ts +++ b/packages/core/src/v3/taskContext/otelProcessors.ts @@ -190,8 +190,7 @@ export class TaskContextMetricExporter implements PushMetricExporter { } if (taskContext.conversationId) { - contextAttrs[SemanticInternalAttributes.GEN_AI_CONVERSATION_ID] = - taskContext.conversationId; + contextAttrs[SemanticInternalAttributes.GEN_AI_CONVERSATION_ID] = taskContext.conversationId; } const modified: ResourceMetrics = { @@ -288,7 +287,10 @@ export class BufferingMetricExporter implements PushMetricExporter { const base = batch[0]!; // Merge all scopeMetrics by scope name, then metrics by descriptor name - const scopeMap = new Map }>(); + const scopeMap = new Map< + string, + { scope: ScopeMetrics["scope"]; metricsMap: Map } + >(); for (const rm of batch) { for (const sm of rm.scopeMetrics) { diff --git a/packages/core/src/v3/test/mock-task-context.ts b/packages/core/src/v3/test/mock-task-context.ts index 66e5849001..0da633c6a7 100644 --- a/packages/core/src/v3/test/mock-task-context.ts +++ b/packages/core/src/v3/test/mock-task-context.ts @@ -258,15 +258,13 @@ export async function runInMockTaskContext( }, locals: { get: (key: LocalsKey) => localsManager.getLocal(key), - set: (key: LocalsKey, value: TValue) => - localsManager.setLocal(key, value), + set: (key: LocalsKey, value: TValue) => localsManager.setLocal(key, value), }, sessions: { in: { send: (sessionId, data, io = "in") => sessionStreamManager.__sendFromTest(sessionId, io, data), - close: (sessionId, io = "in") => - sessionStreamManager.__closeFromTest(sessionId, io), + close: (sessionId, io = "in") => sessionStreamManager.__closeFromTest(sessionId, io), }, }, ctx, diff --git a/packages/core/src/v3/test/test-input-stream-manager.ts b/packages/core/src/v3/test/test-input-stream-manager.ts index 933b92d07c..4a8bbf20fe 100644 --- a/packages/core/src/v3/test/test-input-stream-manager.ts +++ b/packages/core/src/v3/test/test-input-stream-manager.ts @@ -184,9 +184,7 @@ export class TestInputStreamManager implements InputStreamManager { } if (hasHandlers) { - await Promise.all( - Array.from(handlers!).map((h) => Promise.resolve().then(() => h(data))) - ); + await Promise.all(Array.from(handlers!).map((h) => Promise.resolve().then(() => h(data)))); } } diff --git a/packages/core/src/v3/test/test-session-stream-manager.ts b/packages/core/src/v3/test/test-session-stream-manager.ts index f19e01dc33..388c6dca87 100644 --- a/packages/core/src/v3/test/test-session-stream-manager.ts +++ b/packages/core/src/v3/test/test-session-stream-manager.ts @@ -4,10 +4,7 @@ import { InputStreamTimeoutError, } from "../inputStreams/types.js"; import type { InputStreamOnceOptions } from "../realtimeStreams/types.js"; -import type { - SessionChannelIO, - SessionStreamManager, -} from "../sessionStreams/types.js"; +import type { SessionChannelIO, SessionStreamManager } from "../sessionStreams/types.js"; type OnceWaiter = { resolve: (value: InputStreamOnceResult) => void; @@ -40,11 +37,7 @@ export class TestSessionStreamManager implements SessionStreamManager { private buffer = new Map(); private seqNums = new Map(); - on( - sessionId: string, - io: SessionChannelIO, - handler: Handler - ): { off: () => void } { + on(sessionId: string, io: SessionChannelIO, handler: Handler): { off: () => void } { const key = keyFor(sessionId, io); let set = this.handlers.get(key); @@ -167,11 +160,7 @@ export class TestSessionStreamManager implements SessionStreamManager { return undefined; } - setLastDispatchedSeqNum( - _sessionId: string, - _io: SessionChannelIO, - _seqNum: number - ): void { + setLastDispatchedSeqNum(_sessionId: string, _io: SessionChannelIO, _seqNum: number): void { // no-op β€” see comment on `lastDispatchedSeqNum`. } @@ -242,11 +231,7 @@ export class TestSessionStreamManager implements SessionStreamManager { * resolves. Consumption is decided on the synchronous return value, * exactly like production. */ - async __sendFromTest( - sessionId: string, - io: SessionChannelIO, - data: unknown - ): Promise { + async __sendFromTest(sessionId: string, io: SessionChannelIO, data: unknown): Promise { const key = keyFor(sessionId, io); const waiters = this.onceWaiters.get(key); diff --git a/packages/core/src/v3/timeout/api.ts b/packages/core/src/v3/timeout/api.ts index dd211bc42c..864a46bbfe 100644 --- a/packages/core/src/v3/timeout/api.ts +++ b/packages/core/src/v3/timeout/api.ts @@ -47,7 +47,9 @@ export class TimeoutAPI implements TimeoutManager { this.disable(); } - public registerListener(listener: (timeoutInSeconds: number, elapsedTimeInSeconds: number) => void | Promise) { + public registerListener( + listener: (timeoutInSeconds: number, elapsedTimeInSeconds: number) => void | Promise + ) { const manager = this.#getManager(); if (manager.registerListener) { manager.registerListener(listener); diff --git a/packages/core/src/v3/timeout/types.ts b/packages/core/src/v3/timeout/types.ts index 6978689676..03f773c2e5 100644 --- a/packages/core/src/v3/timeout/types.ts +++ b/packages/core/src/v3/timeout/types.ts @@ -2,7 +2,9 @@ export interface TimeoutManager { abortAfterTimeout: (timeoutInSeconds?: number) => AbortController; signal?: AbortSignal; reset: () => void; - registerListener?: (listener: (timeoutInSeconds: number, elapsedTimeInSeconds: number) => void | Promise) => void; + registerListener?: ( + listener: (timeoutInSeconds: number, elapsedTimeInSeconds: number) => void | Promise + ) => void; } export class TaskRunExceededMaxDuration extends Error { diff --git a/packages/core/src/v3/types/schemas.ts b/packages/core/src/v3/types/schemas.ts index e5ae9c3d88..b412102992 100644 --- a/packages/core/src/v3/types/schemas.ts +++ b/packages/core/src/v3/types/schemas.ts @@ -73,20 +73,18 @@ export type SchemaWithInputOutput = export type Schema = SchemaWithInputOutput | SchemaWithoutInput; -export type inferSchema = TSchema extends SchemaWithInputOutput< - infer $TIn, - infer $TOut -> - ? { - in: $TIn; - out: $TOut; - } - : TSchema extends SchemaWithoutInput - ? { - in: $InOut; - out: $InOut; - } - : never; +export type inferSchema = + TSchema extends SchemaWithInputOutput + ? { + in: $TIn; + out: $TOut; + } + : TSchema extends SchemaWithoutInput + ? { + in: $InOut; + out: $InOut; + } + : never; export type inferSchemaIn< TSchema extends Schema | undefined, diff --git a/packages/core/src/v3/types/tasks.ts b/packages/core/src/v3/types/tasks.ts index 978a6e5bd0..f0c86de627 100644 --- a/packages/core/src/v3/types/tasks.ts +++ b/packages/core/src/v3/types/tasks.ts @@ -475,21 +475,14 @@ export type BatchRunHandle = TPayload >; -export type RunHandleOutput = TRunHandle extends RunHandle - ? TOutput - : never; +export type RunHandleOutput = + TRunHandle extends RunHandle ? TOutput : never; -export type RunHandlePayload = TRunHandle extends RunHandle - ? TPayload - : never; +export type RunHandlePayload = + TRunHandle extends RunHandle ? TPayload : never; -export type RunHandleTaskIdentifier = TRunHandle extends RunHandle< - infer TTaskIdentifier, - any, - any -> - ? TTaskIdentifier - : never; +export type RunHandleTaskIdentifier = + TRunHandle extends RunHandle ? TTaskIdentifier : never; export type TaskRunResult = | { @@ -507,13 +500,10 @@ export type TaskRunResult = export type AnyTaskRunResult = TaskRunResult; -export type TaskRunResultFromTask = TTask extends Task< - infer TIdentifier, - any, - infer TOutput -> - ? TaskRunResult - : never; +export type TaskRunResultFromTask = + TTask extends Task + ? TaskRunResult + : never; export type BatchResult = { id: string; @@ -668,7 +658,7 @@ export interface Task */ triggerAndSubscribe: ( payload: TInput, - options?: TriggerAndSubscribeOptions, + options?: TriggerAndSubscribeOptions ) => TaskRunPromise; /** @@ -722,33 +712,24 @@ export interface ToolTask< export type AnyTask = Task; -export type TaskPayload = TTask extends Task - ? TInput - : never; +export type TaskPayload = + TTask extends Task ? TInput : never; -export type TaskOutput = TTask extends Task - ? TOutput - : never; +export type TaskOutput = + TTask extends Task ? TOutput : never; -export type TaskOutputHandle = TTask extends Task< - infer TIdentifier, - infer TInput, - infer TOutput -> - ? RunHandle - : never; +export type TaskOutputHandle = + TTask extends Task + ? RunHandle + : never; -export type TaskBatchOutputHandle = TTask extends Task< - infer TIdentifier, - infer TInput, - infer TOutput -> - ? BatchRunHandle - : never; +export type TaskBatchOutputHandle = + TTask extends Task + ? BatchRunHandle + : never; -export type TaskIdentifier = TTask extends Task - ? TIdentifier - : never; +export type TaskIdentifier = + TTask extends Task ? TIdentifier : never; export type TaskFromIdentifier< TTask extends AnyTask, @@ -1087,17 +1068,14 @@ export type RunTypes = { export type AnyRunTypes = RunTypes; -export type InferRunTypes = T extends RunHandle< - infer TTaskIdentifier, - infer TPayload, - infer TOutput -> - ? RunTypes - : T extends BatchedRunHandle - ? RunTypes - : T extends Task - ? RunTypes - : AnyRunTypes; +export type InferRunTypes = + T extends RunHandle + ? RunTypes + : T extends BatchedRunHandle + ? RunTypes + : T extends Task + ? RunTypes + : AnyRunTypes; export type RunHandleFromTypes = RunHandle< TRunTypes["taskIdentifier"], diff --git a/packages/core/src/v3/types/tools.ts b/packages/core/src/v3/types/tools.ts index 8c3aac80bf..b77ef8ffd4 100644 --- a/packages/core/src/v3/types/tools.ts +++ b/packages/core/src/v3/types/tools.ts @@ -8,8 +8,8 @@ export type inferToolParameters = PARAMETERS extends AISchema ? PARAMETERS["_type"] : PARAMETERS extends z.ZodTypeAny - ? z.infer - : never; + ? z.infer + : never; export function convertToolParametersToSchema( toolParameters: TToolParameters diff --git a/packages/core/src/v3/utils/durations.ts b/packages/core/src/v3/utils/durations.ts index 7bec968fe6..f588fcb8f0 100644 --- a/packages/core/src/v3/utils/durations.ts +++ b/packages/core/src/v3/utils/durations.ts @@ -46,8 +46,8 @@ export function formatDurationMilliseconds( units: options?.units ? options.units : milliseconds < 1000 - ? belowOneSecondUnits - : aboveOneSecondUnits, + ? belowOneSecondUnits + : aboveOneSecondUnits, maxDecimalPoints: options?.maxDecimalPoints ?? 1, largest: options?.maxUnits ?? 2, }); diff --git a/packages/core/src/v3/utils/reconnectBackoff.test.ts b/packages/core/src/v3/utils/reconnectBackoff.test.ts index 5de5a2db8e..c0ac540edf 100644 --- a/packages/core/src/v3/utils/reconnectBackoff.test.ts +++ b/packages/core/src/v3/utils/reconnectBackoff.test.ts @@ -48,9 +48,7 @@ describe("computeReconnectDelayMs", () => { it("never exceeds RECONNECT_BACKOFF_MAX_MS + 1000ms (cap + jitter ceiling)", () => { withFixedRandom(0.999, () => { for (let attempt = 0; attempt < 100; attempt++) { - expect(computeReconnectDelayMs(attempt)).toBeLessThan( - RECONNECT_BACKOFF_MAX_MS + 1000 - ); + expect(computeReconnectDelayMs(attempt)).toBeLessThan(RECONNECT_BACKOFF_MAX_MS + 1000); } }); }); diff --git a/packages/core/src/v3/utils/structuredLogger.ts b/packages/core/src/v3/utils/structuredLogger.ts index 1aae399bbf..97049a9090 100644 --- a/packages/core/src/v3/utils/structuredLogger.ts +++ b/packages/core/src/v3/utils/structuredLogger.ts @@ -26,8 +26,8 @@ export class SimpleStructuredLogger implements StructuredLogger { private level: LogLevel = ["1", "true"].includes(process.env.VERBOSE ?? "") ? LogLevel.verbose : ["1", "true"].includes(process.env.DEBUG ?? "") - ? LogLevel.debug - : LogLevel.info, + ? LogLevel.debug + : LogLevel.info, private fields?: Record ) {} diff --git a/packages/core/src/v3/waitpoints/index.ts b/packages/core/src/v3/waitpoints/index.ts index fc70ff57f2..5aa238a57f 100644 --- a/packages/core/src/v3/waitpoints/index.ts +++ b/packages/core/src/v3/waitpoints/index.ts @@ -7,15 +7,11 @@ export class WaitpointTimeoutError extends Error { } } -export class ManualWaitpointPromise extends Promise< - WaitpointTokenTypedResult -> { +export class ManualWaitpointPromise extends Promise> { constructor( executor: ( resolve: ( - value: - | WaitpointTokenTypedResult - | PromiseLike> + value: WaitpointTokenTypedResult | PromiseLike> ) => void, reject: (reason?: any) => void ) => void diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 838ef3c6e7..40a5932e96 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -1418,8 +1418,8 @@ export class TaskExecutor { error instanceof Error ? `${error.name}: ${error.message}` : typeof error === "string" - ? error - : undefined, + ? error + : undefined, stackTrace: error instanceof Error ? error.stack : undefined, }, skippedRetrying, diff --git a/packages/core/test/errors.test.ts b/packages/core/test/errors.test.ts index 4bcf89d7e9..930286996f 100644 --- a/packages/core/test/errors.test.ts +++ b/packages/core/test/errors.test.ts @@ -112,7 +112,9 @@ describe("parseError truncation", () => { expect(parsed.type).toBe("BUILT_IN_ERROR"); if (parsed.type === "BUILT_IN_ERROR") { - const frameLines = parsed.stackTrace.split("\n").filter((l) => l.trimStart().startsWith("at ")); + const frameLines = parsed.stackTrace + .split("\n") + .filter((l) => l.trimStart().startsWith("at ")); expect(frameLines.length).toBe(50); expect(parsed.stackTrace).toContain("frames omitted"); } @@ -139,7 +141,9 @@ describe("sanitizeError truncation", () => { }); if (result.type === "BUILT_IN_ERROR") { - const frameLines = result.stackTrace.split("\n").filter((l) => l.trimStart().startsWith("at ")); + const frameLines = result.stackTrace + .split("\n") + .filter((l) => l.trimStart().startsWith("at ")); expect(frameLines.length).toBe(50); } }); @@ -249,7 +253,7 @@ describe("truncateStack message line bounding", () => { describe("shouldRetryError + shouldLookupRetrySettings", () => { const internal = (code: string): TaskRunError => - ({ type: "INTERNAL_ERROR", code } as TaskRunError); + ({ type: "INTERNAL_ERROR", code }) as TaskRunError; it("retries SIGSEGV (changed from non-retriable) and looks up retry settings", () => { const err = internal("TASK_PROCESS_SIGSEGV"); diff --git a/packages/core/test/externalSpanExporterWrapper.test.ts b/packages/core/test/externalSpanExporterWrapper.test.ts index 4cff69e3ae..9b51653a1e 100644 --- a/packages/core/test/externalSpanExporterWrapper.test.ts +++ b/packages/core/test/externalSpanExporterWrapper.test.ts @@ -62,10 +62,7 @@ describe("ExternalSpanExporterWrapper warm-start regression", () => { manager.traceContext = { external: { traceparent: TRACEPARENT_RUN_A } }; - const wrapper = new ExternalSpanExporterWrapper( - exporter, - "ffffffffffffffffffffffffffffffff" - ); + const wrapper = new ExternalSpanExporterWrapper(exporter, "ffffffffffffffffffffffffffffffff"); manager.traceContext = { external: { traceparent: TRACEPARENT_RUN_B } }; diff --git a/packages/core/test/flattenAttributes.test.ts b/packages/core/test/flattenAttributes.test.ts index 3fd11fa5d0..345a5f42fc 100644 --- a/packages/core/test/flattenAttributes.test.ts +++ b/packages/core/test/flattenAttributes.test.ts @@ -674,45 +674,35 @@ describe("unflattenAttributes", () => { // unflattener used to crash with TypeError when it tried to descend into a // primitive sibling. it("does not throw when a scalar precedes a deeper path at the same prefix", () => { - expect(() => - unflattenAttributes({ "a.b": "scalar", "a.b.c": "value" }) - ).not.toThrow(); + expect(() => unflattenAttributes({ "a.b": "scalar", "a.b.c": "value" })).not.toThrow(); expect(unflattenAttributes({ "a.b": "scalar", "a.b.c": "value" })).toEqual({ a: { b: { c: "value" } }, }); }); it("does not throw when a deeper path precedes a scalar at the same prefix", () => { - expect(() => - unflattenAttributes({ "a.b.c": "value", "a.b": "scalar" }) - ).not.toThrow(); + expect(() => unflattenAttributes({ "a.b.c": "value", "a.b": "scalar" })).not.toThrow(); expect(unflattenAttributes({ "a.b.c": "value", "a.b": "scalar" })).toEqual({ a: { b: "scalar" }, }); }); it("treats an intermediate null sentinel as overwritable when a deeper path follows", () => { - expect(() => - unflattenAttributes({ "a.b": "$@null((", "a.b.c": "value" }) - ).not.toThrow(); + expect(() => unflattenAttributes({ "a.b": "$@null((", "a.b.c": "value" })).not.toThrow(); expect(unflattenAttributes({ "a.b": "$@null((", "a.b.c": "value" })).toEqual({ a: { b: { c: "value" } }, }); }); it("does not throw when a scalar prefix conflicts with a numeric-index path", () => { - expect(() => - unflattenAttributes({ "a.b": "scalar", "a.b.[0]": "indexed" }) - ).not.toThrow(); + expect(() => unflattenAttributes({ "a.b": "scalar", "a.b.[0]": "indexed" })).not.toThrow(); expect(unflattenAttributes({ "a.b": "scalar", "a.b.[0]": "indexed" })).toEqual({ a: { b: ["indexed"] }, }); }); it("converts an existing object slot to an array when a numeric-index path follows", () => { - expect(() => - unflattenAttributes({ "a.b.c": "value", "a.b.[0]": "indexed" }) - ).not.toThrow(); + expect(() => unflattenAttributes({ "a.b.c": "value", "a.b.[0]": "indexed" })).not.toThrow(); expect(unflattenAttributes({ "a.b.c": "value", "a.b.[0]": "indexed" })).toEqual({ a: { b: ["indexed"] }, }); diff --git a/packages/core/test/recordSpanException.test.ts b/packages/core/test/recordSpanException.test.ts index 6b2d194ce2..67e1aa29b0 100644 --- a/packages/core/test/recordSpanException.test.ts +++ b/packages/core/test/recordSpanException.test.ts @@ -6,7 +6,10 @@ function createMockSpan() { return { recordException: vi.fn(), setStatus: vi.fn(), - } as unknown as Span & { recordException: ReturnType; setStatus: ReturnType }; + } as unknown as Span & { + recordException: ReturnType; + setStatus: ReturnType; + }; } describe("recordSpanException", () => { diff --git a/packages/core/test/resourceCatalog.test.ts b/packages/core/test/resourceCatalog.test.ts index a34e9373f0..5d4e770889 100644 --- a/packages/core/test/resourceCatalog.test.ts +++ b/packages/core/test/resourceCatalog.test.ts @@ -82,8 +82,7 @@ describe("StandardResourceCatalog β€” runtime registration via sentinel context" vi.spyOn(console, "warn").mockImplementation(() => {}); const catalog = new StandardResourceCatalog(); - (globalThis as { __catalogRegisterTaskMetadata?: unknown }) - .__catalogRegisterTaskMetadata = ( + (globalThis as { __catalogRegisterTaskMetadata?: unknown }).__catalogRegisterTaskMetadata = ( task: Parameters[0] ) => { catalog.registerTaskMetadata(task); @@ -124,33 +123,26 @@ describe("StandardResourceCatalog β€” runtime registration via sentinel context" catalog.clearCurrentFileContext(); }); - it( - "control: real file context registers without firing the sentinel warning", - async () => { - const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); - const catalog = new StandardResourceCatalog(); + it("control: real file context registers without firing the sentinel warning", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const catalog = new StandardResourceCatalog(); - (globalThis as { __catalogRegisterTaskMetadata?: unknown }) - .__catalogRegisterTaskMetadata = ( - task: Parameters[0] - ) => { - catalog.registerTaskMetadata(task); - }; + (globalThis as { __catalogRegisterTaskMetadata?: unknown }).__catalogRegisterTaskMetadata = ( + task: Parameters[0] + ) => { + catalog.registerTaskMetadata(task); + }; - catalog.setCurrentFileContext( - "/app/dist/lazy-task.entry.mjs", - "src/tasks/lazy-task.ts" - ); - await import("./fixtures/dynamic-task-module.mjs?control"); - catalog.clearCurrentFileContext(); + catalog.setCurrentFileContext("/app/dist/lazy-task.entry.mjs", "src/tasks/lazy-task.ts"); + await import("./fixtures/dynamic-task-module.mjs?control"); + catalog.clearCurrentFileContext(); - const task = catalog.getTask("lazy-task"); - expect(task).toBeDefined(); - expect(task?.filePath).toBe("/app/dist/lazy-task.entry.mjs"); - expect(task?.entryPoint).toBe("src/tasks/lazy-task.ts"); - expect(warn).not.toHaveBeenCalled(); - } - ); + const task = catalog.getTask("lazy-task"); + expect(task).toBeDefined(); + expect(task?.filePath).toBe("/app/dist/lazy-task.entry.mjs"); + expect(task?.entryPoint).toBe("src/tasks/lazy-task.ts"); + expect(warn).not.toHaveBeenCalled(); + }); }); describe("StandardResourceCatalog β€” duplicate task id collisions", () => { diff --git a/packages/core/test/skillCatalog.test.ts b/packages/core/test/skillCatalog.test.ts index 3f1d29bf57..eb00e5b56e 100644 --- a/packages/core/test/skillCatalog.test.ts +++ b/packages/core/test/skillCatalog.test.ts @@ -69,6 +69,11 @@ describe("StandardResourceCatalog β€” skills", () => { catalog.registerSkillMetadata({ id: "pdf", sourcePath: "./skills/pdf" }); catalog.registerSkillMetadata({ id: "researcher", sourcePath: "./skills/researcher" }); - expect(catalog.listSkillManifests().map((s) => s.id).sort()).toEqual(["pdf", "researcher"]); + expect( + catalog + .listSkillManifests() + .map((s) => s.id) + .sort() + ).toEqual(["pdf", "researcher"]); }); }); diff --git a/packages/plugins/src/rbac.ts b/packages/plugins/src/rbac.ts index ca9a1a0494..189252afd5 100644 --- a/packages/plugins/src/rbac.ts +++ b/packages/plugins/src/rbac.ts @@ -397,10 +397,7 @@ export interface RoleBaseAccessController { // Org-scoped only β€” project-scoped reads still go through getUserRole. // Returns a Map keyed by userId; users with no resolvable role map to // null. The default fallback returns a Map of all userIds β†’ null. - getUserRoles( - userIds: string[], - organizationId: string - ): Promise>; + getUserRoles(userIds: string[], organizationId: string): Promise>; setUserRole(params: { userId: string; @@ -422,9 +419,7 @@ export interface RoleBaseAccessController { // Mutation result for role create/update β€” success carries the new // `role`, failure carries a user-facing `error` string. -export type RoleMutationResult = - | { ok: true; role: Role } - | { ok: false; error: string }; +export type RoleMutationResult = { ok: true; role: Role } | { ok: false; error: string }; // Result for assignment / deletion mutations that don't return a value. export type RoleAssignmentResult = { ok: true } | { ok: false; error: string }; diff --git a/packages/plugins/src/sso.ts b/packages/plugins/src/sso.ts index 7939b7b70d..4805195e41 100644 --- a/packages/plugins/src/sso.ts +++ b/packages/plugins/src/sso.ts @@ -32,9 +32,7 @@ export type OrgSsoStatus = { }>; }; -export type SsoRouteDecision = - | { kind: "no_sso" } - | { kind: "sso_required"; idpOrgId: string }; +export type SsoRouteDecision = { kind: "no_sso" } | { kind: "sso_required"; idpOrgId: string }; export const SSO_FLOWS = [ "user_initiated", @@ -65,10 +63,7 @@ export type SsoResolutionDecision = export type SsoDecisionError = "internal"; -export type SsoBeginError = - | "no_org_for_domain" - | "no_active_connection" - | "feature_disabled"; +export type SsoBeginError = "no_org_for_domain" | "no_active_connection" | "feature_disabled"; export type SsoCompleteError = | "state_replayed_or_expired" diff --git a/packages/redis-worker/package.json b/packages/redis-worker/package.json index 22b8a1b62d..9c4b228477 100644 --- a/packages/redis-worker/package.json +++ b/packages/redis-worker/package.json @@ -54,4 +54,4 @@ "require": "./dist/index.cjs" } } -} \ No newline at end of file +} diff --git a/packages/redis-worker/src/fair-queue/index.ts b/packages/redis-worker/src/fair-queue/index.ts index 0c0b7921a6..219dde5891 100644 --- a/packages/redis-worker/src/fair-queue/index.ts +++ b/packages/redis-worker/src/fair-queue/index.ts @@ -192,7 +192,6 @@ export class FairQueue { shardCount: this.shardCount, }); - if (options.concurrencyGroups && options.concurrencyGroups.length > 0) { this.concurrencyManager = new ConcurrencyManager({ redis: options.redis, @@ -1248,11 +1247,11 @@ export class FairQueue { } const descriptor: QueueDescriptor = storedMessage - ? this.queueDescriptorCache.get(queueId) ?? { + ? (this.queueDescriptorCache.get(queueId) ?? { id: queueId, tenantId: storedMessage.tenantId, metadata: storedMessage.metadata ?? {}, - } + }) : { id: queueId, tenantId: this.keys.extractTenantId(queueId), metadata: {} }; // Complete in visibility manager @@ -1264,10 +1263,7 @@ export class FairQueue { } // Update both old and new indexes, clean up caches if queue is empty - const { queueEmpty } = await this.#updateAllIndexesAfterDequeue( - queueId, - descriptor.tenantId - ); + const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(queueId, descriptor.tenantId); if (queueEmpty) { this.queueDescriptorCache.delete(queueId); this.queueCooloffStates.delete(queueId); @@ -1306,11 +1302,11 @@ export class FairQueue { } const descriptor: QueueDescriptor = storedMessage - ? this.queueDescriptorCache.get(queueId) ?? { + ? (this.queueDescriptorCache.get(queueId) ?? { id: queueId, tenantId: storedMessage.tenantId, metadata: storedMessage.metadata ?? {}, - } + }) : { id: queueId, tenantId: this.keys.extractTenantId(queueId), metadata: {} }; // Release back to queue (visibility manager updates dispatch indexes atomically) diff --git a/packages/redis-worker/src/fair-queue/scheduler.ts b/packages/redis-worker/src/fair-queue/scheduler.ts index 00a22bb97a..318424b7cd 100644 --- a/packages/redis-worker/src/fair-queue/scheduler.ts +++ b/packages/redis-worker/src/fair-queue/scheduler.ts @@ -114,4 +114,3 @@ export class NoopScheduler extends BaseScheduler { return []; } } - diff --git a/packages/redis-worker/src/fair-queue/schedulers/drr.ts b/packages/redis-worker/src/fair-queue/schedulers/drr.ts index 3e05ae8a34..43d0c01dbb 100644 --- a/packages/redis-worker/src/fair-queue/schedulers/drr.ts +++ b/packages/redis-worker/src/fair-queue/schedulers/drr.ts @@ -103,9 +103,7 @@ export class DRRScheduler extends BaseScheduler { ); // Filter out tenants at capacity or with no deficit - const eligibleTenants = tenantData.filter( - (t) => !t.isAtCapacity && t.deficit >= 1 - ); + const eligibleTenants = tenantData.filter((t) => !t.isAtCapacity && t.deficit >= 1); // Log tenants blocked by capacity const blockedTenants = tenantData.filter((t) => t.isAtCapacity); @@ -451,11 +449,6 @@ declare module "@internal/redis" { drrDecrementDeficit(deficitKey: string, tenantId: string): Promise; - drrDecrementDeficitBatch( - deficitKey: string, - tenantId: string, - count: string - ): Promise; + drrDecrementDeficitBatch(deficitKey: string, tenantId: string, count: string): Promise; } } - diff --git a/packages/redis-worker/src/fair-queue/schedulers/index.ts b/packages/redis-worker/src/fair-queue/schedulers/index.ts index bef962f58d..328cdf0ec3 100644 --- a/packages/redis-worker/src/fair-queue/schedulers/index.ts +++ b/packages/redis-worker/src/fair-queue/schedulers/index.ts @@ -5,4 +5,3 @@ export { DRRScheduler } from "./drr.js"; export { WeightedScheduler } from "./weighted.js"; export { RoundRobinScheduler } from "./roundRobin.js"; - diff --git a/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts b/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts index ac55967352..4e7d740d4b 100644 --- a/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts +++ b/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts @@ -154,4 +154,3 @@ export class RoundRobinScheduler extends BaseScheduler { return [...array.slice(normalizedIndex), ...array.slice(0, normalizedIndex)]; } } - diff --git a/packages/redis-worker/src/fair-queue/schedulers/weighted.ts b/packages/redis-worker/src/fair-queue/schedulers/weighted.ts index de0d45d3cf..7b44a7df17 100644 --- a/packages/redis-worker/src/fair-queue/schedulers/weighted.ts +++ b/packages/redis-worker/src/fair-queue/schedulers/weighted.ts @@ -80,11 +80,7 @@ export class WeightedScheduler extends BaseScheduler { consumerId: string, context: SchedulerContext ): Promise { - const snapshot = await this.#getOrCreateSnapshot( - masterQueueShard, - consumerId, - context - ); + const snapshot = await this.#getOrCreateSnapshot(masterQueueShard, consumerId, context); if (snapshot.queues.length === 0) { return []; @@ -431,4 +427,3 @@ export class WeightedScheduler extends BaseScheduler { return result; } } - diff --git a/packages/redis-worker/src/fair-queue/telemetry.ts b/packages/redis-worker/src/fair-queue/telemetry.ts index e9531fc812..fa47ed93c0 100644 --- a/packages/redis-worker/src/fair-queue/telemetry.ts +++ b/packages/redis-worker/src/fair-queue/telemetry.ts @@ -334,7 +334,6 @@ export class FairQueueTelemetry { } }); } - } // ============================================================================ diff --git a/packages/redis-worker/src/fair-queue/tests/drr.test.ts b/packages/redis-worker/src/fair-queue/tests/drr.test.ts index 66575d6486..e0f1dc7875 100644 --- a/packages/redis-worker/src/fair-queue/tests/drr.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/drr.test.ts @@ -9,111 +9,127 @@ describe("DRRScheduler", () => { let keys: FairQueueKeyProducer; describe("deficit management", () => { - redisTest("should initialize deficit to 0 for new tenants", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + redisTest( + "should initialize deficit to 0 for new tenants", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const deficit = await scheduler.getDeficit("new-tenant"); - expect(deficit).toBe(0); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - await scheduler.close(); - }); + const deficit = await scheduler.getDeficit("new-tenant"); + expect(deficit).toBe(0); - redisTest("should add quantum atomically with capping", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + await scheduler.close(); + } + ); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + redisTest( + "should add quantum atomically with capping", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - // Setup: put queues in the master shard - const masterKey = keys.masterQueueKey(0); - const now = Date.now(); - - await redis.zadd(masterKey, now, "tenant:t1:queue:q1"); - - // Create context mock - const context: SchedulerContext = { - getCurrentConcurrency: async () => 0, - getConcurrencyLimit: async () => 100, - isAtCapacity: async () => false, - getQueueDescriptor: (queueId) => ({ - id: queueId, - tenantId: keys.extractTenantId(queueId), - metadata: {}, - }), - }; - - // Run multiple iterations to accumulate deficit - for (let i = 0; i < 15; i++) { - await scheduler.selectQueues(masterKey, "consumer-1", context); - } + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - // Deficit should be capped at maxDeficit (50) - const deficit = await scheduler.getDeficit("t1"); - expect(deficit).toBeLessThanOrEqual(50); + // Setup: put queues in the master shard + const masterKey = keys.masterQueueKey(0); + const now = Date.now(); + + await redis.zadd(masterKey, now, "tenant:t1:queue:q1"); + + // Create context mock + const context: SchedulerContext = { + getCurrentConcurrency: async () => 0, + getConcurrencyLimit: async () => 100, + isAtCapacity: async () => false, + getQueueDescriptor: (queueId) => ({ + id: queueId, + tenantId: keys.extractTenantId(queueId), + metadata: {}, + }), + }; + + // Run multiple iterations to accumulate deficit + for (let i = 0; i < 15; i++) { + await scheduler.selectQueues(masterKey, "consumer-1", context); + } + + // Deficit should be capped at maxDeficit (50) + const deficit = await scheduler.getDeficit("t1"); + expect(deficit).toBeLessThanOrEqual(50); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); - redisTest("should decrement deficit when processing", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + redisTest( + "should decrement deficit when processing", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - // Manually set some deficit - const deficitKey = `test:drr:deficit`; - await redis.hset(deficitKey, "t1", "10"); + // Manually set some deficit + const deficitKey = `test:drr:deficit`; + await redis.hset(deficitKey, "t1", "10"); - // Record processing - await scheduler.recordProcessed("t1", "queue:q1"); + // Record processing + await scheduler.recordProcessed("t1", "queue:q1"); - const deficit = await scheduler.getDeficit("t1"); - expect(deficit).toBe(9); + const deficit = await scheduler.getDeficit("t1"); + expect(deficit).toBe(9); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); - redisTest("should not go below 0 on decrement", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + redisTest( + "should not go below 0 on decrement", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - const deficitKey = `test:drr:deficit`; - await redis.hset(deficitKey, "t1", "0.5"); + const deficitKey = `test:drr:deficit`; + await redis.hset(deficitKey, "t1", "0.5"); - await scheduler.recordProcessed("t1", "queue:q1"); + await scheduler.recordProcessed("t1", "queue:q1"); - const deficit = await scheduler.getDeficit("t1"); - expect(deficit).toBe(0); + const deficit = await scheduler.getDeficit("t1"); + expect(deficit).toBe(0); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); redisTest( "should decrement deficit by count when using recordProcessedBatch", @@ -198,191 +214,219 @@ describe("DRRScheduler", () => { }); describe("queue selection", () => { - redisTest("should return queues grouped by tenant", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); - - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); - - const masterKey = keys.masterQueueKey(0); - const now = Date.now(); - - // Add queues for different tenants (all timestamps in the past) - await redis.zadd( - masterKey, - now - 200, - "tenant:t1:queue:q1", - now - 100, - "tenant:t1:queue:q2", - now - 50, - "tenant:t2:queue:q1" - ); - - const context: SchedulerContext = { - getCurrentConcurrency: async () => 0, - getConcurrencyLimit: async () => 100, - isAtCapacity: async () => false, - getQueueDescriptor: (queueId) => ({ - id: queueId, - tenantId: keys.extractTenantId(queueId), - metadata: {}, - }), - }; - - const result = await scheduler.selectQueues(masterKey, "consumer-1", context); - - // Should have both tenants - const tenantIds = result.map((r) => r.tenantId); - expect(tenantIds).toContain("t1"); - expect(tenantIds).toContain("t2"); - - // t1 should have 2 queues - const t1 = result.find((r) => r.tenantId === "t1"); - expect(t1?.queues).toHaveLength(2); - - await scheduler.close(); - await redis.quit(); - }); - - redisTest("should filter out tenants at capacity", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + redisTest( + "should return queues grouped by tenant", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - const masterKey = keys.masterQueueKey(0); - const now = Date.now(); + const masterKey = keys.masterQueueKey(0); + const now = Date.now(); + + // Add queues for different tenants (all timestamps in the past) + await redis.zadd( + masterKey, + now - 200, + "tenant:t1:queue:q1", + now - 100, + "tenant:t1:queue:q2", + now - 50, + "tenant:t2:queue:q1" + ); + + const context: SchedulerContext = { + getCurrentConcurrency: async () => 0, + getConcurrencyLimit: async () => 100, + isAtCapacity: async () => false, + getQueueDescriptor: (queueId) => ({ + id: queueId, + tenantId: keys.extractTenantId(queueId), + metadata: {}, + }), + }; + + const result = await scheduler.selectQueues(masterKey, "consumer-1", context); + + // Should have both tenants + const tenantIds = result.map((r) => r.tenantId); + expect(tenantIds).toContain("t1"); + expect(tenantIds).toContain("t2"); + + // t1 should have 2 queues + const t1 = result.find((r) => r.tenantId === "t1"); + expect(t1?.queues).toHaveLength(2); - await redis.zadd(masterKey, now - 100, "tenant:t1:queue:q1", now - 50, "tenant:t2:queue:q1"); + await scheduler.close(); + await redis.quit(); + } + ); - const context: SchedulerContext = { - getCurrentConcurrency: async () => 0, - getConcurrencyLimit: async () => 100, - isAtCapacity: async (_, groupId) => groupId === "t1", // t1 at capacity - getQueueDescriptor: (queueId) => ({ - id: queueId, - tenantId: keys.extractTenantId(queueId), - metadata: {}, - }), - }; + redisTest( + "should filter out tenants at capacity", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const result = await scheduler.selectQueues(masterKey, "consumer-1", context); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - // Only t2 should be returned - const tenantIds = result.map((r) => r.tenantId); - expect(tenantIds).not.toContain("t1"); - expect(tenantIds).toContain("t2"); + const masterKey = keys.masterQueueKey(0); + const now = Date.now(); + + await redis.zadd( + masterKey, + now - 100, + "tenant:t1:queue:q1", + now - 50, + "tenant:t2:queue:q1" + ); + + const context: SchedulerContext = { + getCurrentConcurrency: async () => 0, + getConcurrencyLimit: async () => 100, + isAtCapacity: async (_, groupId) => groupId === "t1", // t1 at capacity + getQueueDescriptor: (queueId) => ({ + id: queueId, + tenantId: keys.extractTenantId(queueId), + metadata: {}, + }), + }; + + const result = await scheduler.selectQueues(masterKey, "consumer-1", context); + + // Only t2 should be returned + const tenantIds = result.map((r) => r.tenantId); + expect(tenantIds).not.toContain("t1"); + expect(tenantIds).toContain("t2"); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); - redisTest("should skip tenants with insufficient deficit", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + redisTest( + "should skip tenants with insufficient deficit", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - const masterKey = keys.masterQueueKey(0); - const now = Date.now(); + const masterKey = keys.masterQueueKey(0); + const now = Date.now(); - await redis.zadd(masterKey, now - 100, "tenant:t1:queue:q1", now - 50, "tenant:t2:queue:q1"); + await redis.zadd( + masterKey, + now - 100, + "tenant:t1:queue:q1", + now - 50, + "tenant:t2:queue:q1" + ); - // Set t1 deficit to 0 (no credits) - const deficitKey = `test:drr:deficit`; - await redis.hset(deficitKey, "t1", "0"); - - const context: SchedulerContext = { - getCurrentConcurrency: async () => 0, - getConcurrencyLimit: async () => 100, - isAtCapacity: async () => false, - getQueueDescriptor: (queueId) => ({ - id: queueId, - tenantId: keys.extractTenantId(queueId), - metadata: {}, - }), - }; - - // First call adds quantum to both tenants - // t1: 0 + 5 = 5, t2: 0 + 5 = 5 - const result = await scheduler.selectQueues(masterKey, "consumer-1", context); - - // Both should be returned (both have deficit >= 1 after quantum added) - const tenantIds = result.map((r) => r.tenantId); - expect(tenantIds).toContain("t1"); - expect(tenantIds).toContain("t2"); + // Set t1 deficit to 0 (no credits) + const deficitKey = `test:drr:deficit`; + await redis.hset(deficitKey, "t1", "0"); + + const context: SchedulerContext = { + getCurrentConcurrency: async () => 0, + getConcurrencyLimit: async () => 100, + isAtCapacity: async () => false, + getQueueDescriptor: (queueId) => ({ + id: queueId, + tenantId: keys.extractTenantId(queueId), + metadata: {}, + }), + }; + + // First call adds quantum to both tenants + // t1: 0 + 5 = 5, t2: 0 + 5 = 5 + const result = await scheduler.selectQueues(masterKey, "consumer-1", context); + + // Both should be returned (both have deficit >= 1 after quantum added) + const tenantIds = result.map((r) => r.tenantId); + expect(tenantIds).toContain("t1"); + expect(tenantIds).toContain("t2"); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); - redisTest("should order tenants by deficit (highest first)", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + redisTest( + "should order tenants by deficit (highest first)", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - const masterKey = keys.masterQueueKey(0); - const now = Date.now(); + const masterKey = keys.masterQueueKey(0); + const now = Date.now(); - await redis.zadd( - masterKey, - now - 300, - "tenant:t1:queue:q1", - now - 200, - "tenant:t2:queue:q1", - now - 100, - "tenant:t3:queue:q1" - ); + await redis.zadd( + masterKey, + now - 300, + "tenant:t1:queue:q1", + now - 200, + "tenant:t2:queue:q1", + now - 100, + "tenant:t3:queue:q1" + ); - // Set different deficits - const deficitKey = `test:drr:deficit`; - await redis.hset(deficitKey, "t1", "10"); - await redis.hset(deficitKey, "t2", "30"); - await redis.hset(deficitKey, "t3", "20"); - - const context: SchedulerContext = { - getCurrentConcurrency: async () => 0, - getConcurrencyLimit: async () => 100, - isAtCapacity: async () => false, - getQueueDescriptor: (queueId) => ({ - id: queueId, - tenantId: keys.extractTenantId(queueId), - metadata: {}, - }), - }; - - const result = await scheduler.selectQueues(masterKey, "consumer-1", context); - - // Should be ordered by deficit: t2 (35), t3 (25), t1 (15) - // (original + quantum of 5) - expect(result[0]?.tenantId).toBe("t2"); - expect(result[1]?.tenantId).toBe("t3"); - expect(result[2]?.tenantId).toBe("t1"); + // Set different deficits + const deficitKey = `test:drr:deficit`; + await redis.hset(deficitKey, "t1", "10"); + await redis.hset(deficitKey, "t2", "30"); + await redis.hset(deficitKey, "t3", "20"); + + const context: SchedulerContext = { + getCurrentConcurrency: async () => 0, + getConcurrencyLimit: async () => 100, + isAtCapacity: async () => false, + getQueueDescriptor: (queueId) => ({ + id: queueId, + tenantId: keys.extractTenantId(queueId), + metadata: {}, + }), + }; + + const result = await scheduler.selectQueues(masterKey, "consumer-1", context); + + // Should be ordered by deficit: t2 (35), t3 (25), t1 (15) + // (original + quantum of 5) + expect(result[0]?.tenantId).toBe("t2"); + expect(result[1]?.tenantId).toBe("t3"); + expect(result[2]?.tenantId).toBe("t1"); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); }); describe("get all deficits", () => { diff --git a/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts b/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts index f1634ad2fe..7a6d7c6a57 100644 --- a/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts @@ -134,7 +134,6 @@ class TestFairQueueHelper { return this.fairQueue.getTotalInflightCount(); } - registerTelemetryGauges(options?: { observedTenants?: string[] }) { return this.fairQueue.registerTelemetryGauges(options); } @@ -1371,5 +1370,4 @@ describe("FairQueue", () => { } ); }); - }); diff --git a/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts b/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts index 4d85603b98..955cc49457 100644 --- a/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts @@ -11,7 +11,12 @@ import { WorkerQueueManager, FixedDelayRetry, } from "../index.js"; -import type { FairQueueKeyProducer, FairQueueOptions, QueueDescriptor, StoredMessage } from "../types.js"; +import type { + FairQueueKeyProducer, + FairQueueOptions, + QueueDescriptor, + StoredMessage, +} from "../types.js"; import { createRedisClient, type RedisOptions } from "@internal/redis"; const TestPayloadSchema = z.object({ id: z.number(), value: z.string() }); diff --git a/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts b/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts index b26fb98119..695d5e462a 100644 --- a/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts @@ -801,10 +801,7 @@ describe("Two-Level Tenant Dispatch", () => { })(); // Wait for t2's messages to be processed (they shouldn't be blocked by t1) - await waitFor( - () => processed.filter((p) => p.tenantId === "t2").length === 3, - 10000 - ); + await waitFor(() => processed.filter((p) => p.tenantId === "t2").length === 3, 10000); const t2ProcessedCount = processed.filter((p) => p.tenantId === "t2").length; expect(t2ProcessedCount).toBe(3); diff --git a/packages/redis-worker/src/fair-queue/tests/visibility.test.ts b/packages/redis-worker/src/fair-queue/tests/visibility.test.ts index 2ecdeb41e9..0a0f7c7f44 100644 --- a/packages/redis-worker/src/fair-queue/tests/visibility.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/visibility.test.ts @@ -41,7 +41,13 @@ describe("VisibilityManager", () => { await redis.hset(queueItemsKey, messageId, JSON.stringify(storedMessage)); // Claim the message (moves it to in-flight set) - const claimResult = await manager.claim(queueId, queueKey, queueItemsKey, "consumer-1", 5000); + const claimResult = await manager.claim( + queueId, + queueKey, + queueItemsKey, + "consumer-1", + 5000 + ); expect(claimResult.claimed).toBe(true); // Heartbeat should succeed since message is in-flight @@ -110,7 +116,13 @@ describe("VisibilityManager", () => { await redis.zadd(queueKey, storedMessage.timestamp, messageId); await redis.hset(queueItemsKey, messageId, JSON.stringify(storedMessage)); - const claimResult = await manager.claim(queueId, queueKey, queueItemsKey, "consumer-1", 5000); + const claimResult = await manager.claim( + queueId, + queueKey, + queueItemsKey, + "consumer-1", + 5000 + ); expect(claimResult.claimed).toBe(true); // Heartbeat should work before complete @@ -358,7 +370,13 @@ describe("VisibilityManager", () => { } // Request 10 messages but only 3 exist - const claimed = await manager.claimBatch(queueId, queueKey, queueItemsKey, "consumer-1", 10); + const claimed = await manager.claimBatch( + queueId, + queueKey, + queueItemsKey, + "consumer-1", + 10 + ); expect(claimed).toHaveLength(3); expect(claimed[0]!.messageId).toBe("msg-1"); @@ -546,7 +564,15 @@ describe("VisibilityManager", () => { const dispatchKey = keys.dispatchKey(0); // Should not throw when releasing empty array - await manager.releaseBatch([], queueId, queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, "t1"); + await manager.releaseBatch( + [], + queueId, + queueKey, + queueItemsKey, + tenantQueueIndexKey, + dispatchKey, + "t1" + ); await manager.close(); } @@ -591,7 +617,15 @@ describe("VisibilityManager", () => { const claimed = await manager.claimBatch(queueId, queueKey, queueItemsKey, "consumer-1", 3); // Release all messages back - await manager.releaseBatch(claimed, queueId, queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, "t1"); + await manager.releaseBatch( + claimed, + queueId, + queueKey, + queueItemsKey, + tenantQueueIndexKey, + dispatchKey, + "t1" + ); // Tenant queue index should have the queue with correct score const tenantQueueScore = await redis.zscore(tenantQueueIndexKey, queueId); @@ -644,7 +678,13 @@ describe("VisibilityManager", () => { await redis.hset(queueItemsKey, messageId, JSON.stringify(storedMessage)); // Claim with very short timeout - const claimResult = await manager.claim(queueId, queueKey, queueItemsKey, "consumer-1", 100); + const claimResult = await manager.claim( + queueId, + queueKey, + queueItemsKey, + "consumer-1", + 100 + ); expect(claimResult.claimed).toBe(true); // Wait for timeout to expire @@ -836,7 +876,13 @@ describe("VisibilityManager", () => { await redis.hset(queueItemsKey, messageId, JSON.stringify(storedMessage)); // Claim the message - const claimResult = await manager.claim(queueId, queueKey, queueItemsKey, "consumer-1", 100); + const claimResult = await manager.claim( + queueId, + queueKey, + queueItemsKey, + "consumer-1", + 100 + ); expect(claimResult.claimed).toBe(true); // Corrupt the in-flight data by setting invalid JSON @@ -868,4 +914,3 @@ describe("VisibilityManager", () => { ); }); }); - diff --git a/packages/redis-worker/src/fair-queue/visibility.ts b/packages/redis-worker/src/fair-queue/visibility.ts index a182a4e790..40c7ce5919 100644 --- a/packages/redis-worker/src/fair-queue/visibility.ts +++ b/packages/redis-worker/src/fair-queue/visibility.ts @@ -45,8 +45,8 @@ export class VisibilityManager { this.shardCount = options.shardCount; this.defaultTimeoutMs = options.defaultTimeoutMs; this.logger = options.logger ?? { - debug: () => { }, - error: () => { }, + debug: () => {}, + error: () => {}, }; this.#registerCommands(); diff --git a/packages/redis-worker/src/mollifier/buffer.test.ts b/packages/redis-worker/src/mollifier/buffer.test.ts index 3a775bbb8f..00716e5d54 100644 --- a/packages/redis-worker/src/mollifier/buffer.test.ts +++ b/packages/redis-worker/src/mollifier/buffer.test.ts @@ -42,9 +42,7 @@ describe("mollifierReconnectDelayMs", () => { }); it("decorrelates concurrent reconnects (distinct values across random draws)", () => { - const draws = [0.05, 0.3, 0.55, 0.8, 0.95].map((r) => - mollifierReconnectDelayMs(20, () => r), - ); + const draws = [0.05, 0.3, 0.55, 0.8, 0.95].map((r) => mollifierReconnectDelayMs(20, () => r)); // Lockstep would collapse to a single value; jitter spreads them. expect(new Set(draws).size).toBeGreaterThan(1); }); @@ -127,107 +125,123 @@ describe("MollifierBuffer construction", () => { }); describe("MollifierBuffer.accept", () => { - redisTest("accept writes entry, enqueues, and tracks env", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "accept writes entry, enqueues, and tracks env", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - await buffer.accept({ - runId: "run_1", - envId: "env_a", - orgId: "org_1", - payload: serialiseSnapshot({ taskId: "t" }), - }); - - const entry = await buffer.getEntry("run_1"); - expect(entry).not.toBeNull(); - expect(entry!.runId).toBe("run_1"); - expect(entry!.envId).toBe("env_a"); - expect(entry!.orgId).toBe("org_1"); - expect(entry!.status).toBe("QUEUED"); - expect(entry!.attempts).toBe(0); - expect(entry!.createdAt).toBeInstanceOf(Date); - - const envs = await buffer.listEnvsForOrg("org_1"); - expect(envs).toContain("env_a"); - } finally { - await buffer.close(); + try { + await buffer.accept({ + runId: "run_1", + envId: "env_a", + orgId: "org_1", + payload: serialiseSnapshot({ taskId: "t" }), + }); + + const entry = await buffer.getEntry("run_1"); + expect(entry).not.toBeNull(); + expect(entry!.runId).toBe("run_1"); + expect(entry!.envId).toBe("env_a"); + expect(entry!.orgId).toBe("org_1"); + expect(entry!.status).toBe("QUEUED"); + expect(entry!.attempts).toBe(0); + expect(entry!.createdAt).toBeInstanceOf(Date); + + const envs = await buffer.listEnvsForOrg("org_1"); + expect(envs).toContain("env_a"); + } finally { + await buffer.close(); + } } - }); + ); }); describe("MollifierBuffer.pop", () => { - redisTest("pop returns next QUEUED entry and transitions to DRAINING", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "pop returns next QUEUED entry and transitions to DRAINING", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - await buffer.accept({ runId: "run_1", envId: "env_a", orgId: "org_1", payload: "{}" }); - await buffer.accept({ runId: "run_2", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "run_1", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.accept({ runId: "run_2", envId: "env_a", orgId: "org_1", payload: "{}" }); - const popped = await buffer.pop("env_a"); - expect(popped).not.toBeNull(); - expect(popped!.runId).toBe("run_1"); - expect(popped!.status).toBe("DRAINING"); + const popped = await buffer.pop("env_a"); + expect(popped).not.toBeNull(); + expect(popped!.runId).toBe("run_1"); + expect(popped!.status).toBe("DRAINING"); - const stored = await buffer.getEntry("run_1"); - expect(stored!.status).toBe("DRAINING"); - } finally { - await buffer.close(); + const stored = await buffer.getEntry("run_1"); + expect(stored!.status).toBe("DRAINING"); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("pop returns null when env queue is empty", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "pop returns null when env queue is empty", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - const popped = await buffer.pop("env_nonexistent"); - expect(popped).toBeNull(); - } finally { - await buffer.close(); + try { + const popped = await buffer.pop("env_nonexistent"); + expect(popped).toBeNull(); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("atomic RPOP across two parallel pops on the same env", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "atomic RPOP across two parallel pops on the same env", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - await buffer.accept({ runId: "only", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "only", envId: "env_a", orgId: "org_1", payload: "{}" }); - const [a, b] = await Promise.all([buffer.pop("env_a"), buffer.pop("env_a")]); - const winners = [a, b].filter((x) => x !== null); - expect(winners).toHaveLength(1); - expect(winners[0]!.runId).toBe("only"); - } finally { - await buffer.close(); + const [a, b] = await Promise.all([buffer.pop("env_a"), buffer.pop("env_a")]); + const winners = [a, b].filter((x) => x !== null); + expect(winners).toHaveLength(1); + expect(winners[0]!.runId).toBe("only"); + } finally { + await buffer.close(); + } } - }); + ); }); describe("MollifierBuffer.ack", () => { @@ -261,7 +275,7 @@ describe("MollifierBuffer.ack", () => { } finally { await buffer.close(); } - }, + } ); redisTest("ack on missing entry is a no-op", { timeout: 20_000 }, async ({ redisContainer }) => { @@ -318,7 +332,7 @@ describe("MollifierBuffer.pop orphan handling", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -364,36 +378,40 @@ describe("MollifierBuffer.pop orphan handling", () => { } finally { await buffer.close(); } - }, + } ); }); describe("MollifierBuffer.requeue", () => { - redisTest("requeue increments attempts, restores QUEUED, re-LPUSHes", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "requeue increments attempts, restores QUEUED, re-LPUSHes", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - await buffer.accept({ runId: "run_r", envId: "env_a", orgId: "org_1", payload: "{}" }); - await buffer.pop("env_a"); - await buffer.requeue("run_r"); + try { + await buffer.accept({ runId: "run_r", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.pop("env_a"); + await buffer.requeue("run_r"); - const entry = await buffer.getEntry("run_r"); - expect(entry!.status).toBe("QUEUED"); - expect(entry!.attempts).toBe(1); + const entry = await buffer.getEntry("run_r"); + expect(entry!.status).toBe("QUEUED"); + expect(entry!.attempts).toBe(1); - const popped = await buffer.pop("env_a"); - expect(popped!.runId).toBe("run_r"); - } finally { - await buffer.close(); + const popped = await buffer.pop("env_a"); + expect(popped!.runId).toBe("run_r"); + } finally { + await buffer.close(); + } } - }); + ); }); describe("MollifierBuffer.fail", () => { @@ -432,7 +450,7 @@ describe("MollifierBuffer.fail", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -460,7 +478,7 @@ describe("MollifierBuffer.fail", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -516,7 +534,7 @@ describe("MollifierBuffer.fail", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -548,7 +566,7 @@ describe("MollifierBuffer TTL", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -567,7 +585,7 @@ describe("MollifierBuffer payload encoding", () => { }); const tricky = { - quotes: 'a"b\'c', + quotes: "a\"b'c", backslash: "x\\y\\z", newlines: "line1\nline2\r\nline3", tab: "col1\tcol2", @@ -589,7 +607,7 @@ describe("MollifierBuffer payload encoding", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -618,7 +636,7 @@ describe("MollifierBuffer.requeue on missing entry", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -663,7 +681,7 @@ describe("MollifierBuffer.requeue ordering", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -674,127 +692,147 @@ describe("MollifierBuffer.evaluateTrip", () => { holdMs: 100, }; - redisTest("under threshold: not tripped, count increments", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "under threshold: not tripped, count increments", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - const r1 = await buffer.evaluateTrip("env_a", tripOptions); - expect(r1).toEqual({ tripped: false, count: 1 }); + try { + const r1 = await buffer.evaluateTrip("env_a", tripOptions); + expect(r1).toEqual({ tripped: false, count: 1 }); - const r2 = await buffer.evaluateTrip("env_a", tripOptions); - expect(r2).toEqual({ tripped: false, count: 2 }); - } finally { - await buffer.close(); + const r2 = await buffer.evaluateTrip("env_a", tripOptions); + expect(r2).toEqual({ tripped: false, count: 2 }); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("crossing threshold sets the tripped marker", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "crossing threshold sets the tripped marker", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - for (let i = 0; i < 5; i++) { - const r = await buffer.evaluateTrip("env_a", tripOptions); - expect(r.tripped).toBe(false); - } + try { + for (let i = 0; i < 5; i++) { + const r = await buffer.evaluateTrip("env_a", tripOptions); + expect(r.tripped).toBe(false); + } - const after = await buffer.evaluateTrip("env_a", tripOptions); - expect(after).toEqual({ tripped: true, count: 6 }); + const after = await buffer.evaluateTrip("env_a", tripOptions); + expect(after).toEqual({ tripped: true, count: 6 }); - const sticky = await buffer.evaluateTrip("env_a", tripOptions); - expect(sticky.tripped).toBe(true); - } finally { - await buffer.close(); + const sticky = await buffer.evaluateTrip("env_a", tripOptions); + expect(sticky.tripped).toBe(true); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("hold-down marker expires after holdMs and env resets", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "hold-down marker expires after holdMs and env resets", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - const fastWindow = { windowMs: 100, threshold: 2, holdMs: 100 }; - await buffer.evaluateTrip("env_a", fastWindow); - await buffer.evaluateTrip("env_a", fastWindow); - const tripped = await buffer.evaluateTrip("env_a", fastWindow); - expect(tripped.tripped).toBe(true); + try { + const fastWindow = { windowMs: 100, threshold: 2, holdMs: 100 }; + await buffer.evaluateTrip("env_a", fastWindow); + await buffer.evaluateTrip("env_a", fastWindow); + const tripped = await buffer.evaluateTrip("env_a", fastWindow); + expect(tripped.tripped).toBe(true); - // Wait past windowMs AND holdMs so both rate counter and tripped marker expire - await new Promise((r) => setTimeout(r, 220)); + // Wait past windowMs AND holdMs so both rate counter and tripped marker expire + await new Promise((r) => setTimeout(r, 220)); - const recovered = await buffer.evaluateTrip("env_a", fastWindow); - expect(recovered).toEqual({ tripped: false, count: 1 }); - } finally { - await buffer.close(); + const recovered = await buffer.evaluateTrip("env_a", fastWindow); + expect(recovered).toEqual({ tripped: false, count: 1 }); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("env isolation: tripping env_a does not affect env_b", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "env isolation: tripping env_a does not affect env_b", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - for (let i = 0; i < 6; i++) { - await buffer.evaluateTrip("env_a", tripOptions); - } - const aTripped = await buffer.evaluateTrip("env_a", tripOptions); - expect(aTripped.tripped).toBe(true); + try { + for (let i = 0; i < 6; i++) { + await buffer.evaluateTrip("env_a", tripOptions); + } + const aTripped = await buffer.evaluateTrip("env_a", tripOptions); + expect(aTripped.tripped).toBe(true); - const b = await buffer.evaluateTrip("env_b", tripOptions); - expect(b).toEqual({ tripped: false, count: 1 }); - } finally { - await buffer.close(); + const b = await buffer.evaluateTrip("env_b", tripOptions); + expect(b).toEqual({ tripped: false, count: 1 }); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("window expires and counter resets when no traffic", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "window expires and counter resets when no traffic", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - const fastWindow = { windowMs: 100, threshold: 100, holdMs: 100 }; - await buffer.evaluateTrip("env_x", fastWindow); - await buffer.evaluateTrip("env_x", fastWindow); - // both incremented within a fresh window β€” count should be 2 - - await new Promise((r) => setTimeout(r, 150)); - const fresh = await buffer.evaluateTrip("env_x", fastWindow); - expect(fresh.count).toBe(1); - } finally { - await buffer.close(); + try { + const fastWindow = { windowMs: 100, threshold: 100, holdMs: 100 }; + await buffer.evaluateTrip("env_x", fastWindow); + await buffer.evaluateTrip("env_x", fastWindow); + // both incremented within a fresh window β€” count should be 2 + + await new Promise((r) => setTimeout(r, 150)); + const fresh = await buffer.evaluateTrip("env_x", fastWindow); + expect(fresh.count).toBe(1); + } finally { + await buffer.close(); + } } - }); + ); redisTest( "tripped marker outlives the rate counter window", @@ -825,7 +863,7 @@ describe("MollifierBuffer.evaluateTrip", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -846,7 +884,7 @@ describe("MollifierBuffer.evaluateTrip", () => { // so trip semantics don't interfere with the count assertion. const opts = { windowMs: 5000, threshold: 1_000_000, holdMs: 100 }; const results = await Promise.all( - Array.from({ length: 100 }, () => buffer.evaluateTrip("env_atomic", opts)), + Array.from({ length: 100 }, () => buffer.evaluateTrip("env_atomic", opts)) ); // Every return value is unique (no two callers saw the same INCR result). @@ -858,7 +896,7 @@ describe("MollifierBuffer.evaluateTrip", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -891,7 +929,7 @@ describe("MollifierBuffer entry lifecycle invariants", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -944,7 +982,7 @@ describe("MollifierBuffer entry lifecycle invariants", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -994,7 +1032,7 @@ describe("MollifierBuffer.accept idempotency", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1016,7 +1054,12 @@ describe("MollifierBuffer.accept idempotency", () => { const stored = await buffer.getEntry("run_dr"); expect(stored!.status).toBe("DRAINING"); - const dup = await buffer.accept({ runId: "run_dr", envId: "env_a", orgId: "org_1", payload: "{}" }); + const dup = await buffer.accept({ + runId: "run_dr", + envId: "env_a", + orgId: "org_1", + payload: "{}", + }); expect(dup).toEqual({ kind: "duplicate_run_id" }); const afterDup = await buffer.getEntry("run_dr"); @@ -1024,7 +1067,7 @@ describe("MollifierBuffer.accept idempotency", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1067,7 +1110,7 @@ describe("MollifierBuffer.accept idempotency", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1114,7 +1157,7 @@ describe("MollifierBuffer.accept idempotency", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -1141,7 +1184,7 @@ describe("MollifierBuffer envs set lifecycle", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1170,7 +1213,7 @@ describe("MollifierBuffer envs set lifecycle", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1198,7 +1241,7 @@ describe("MollifierBuffer envs set lifecycle", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -1247,7 +1290,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1292,7 +1335,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1325,7 +1368,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1350,7 +1393,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1386,7 +1429,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1424,7 +1467,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1482,7 +1525,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1507,7 +1550,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1550,7 +1593,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1571,7 +1614,13 @@ describe("MollifierBuffer idempotency lookup", () => { }); const idem = { idempotencyKey: "kheal", taskIdentifier: "t" }; try { - await buffer.accept({ runId: "heal_old", envId: "env_h", orgId: "org_1", payload: "{}", ...idem }); + await buffer.accept({ + runId: "heal_old", + envId: "env_h", + orgId: "org_1", + payload: "{}", + ...idem, + }); // Simulate eviction of the entry hash while the lookup survives. await buffer["redis"].del("mollifier:entries:heal_old"); const lookupKey = idempotencyLookupKeyFor({ envId: "env_h", ...idem }); @@ -1591,7 +1640,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1610,7 +1659,13 @@ describe("MollifierBuffer idempotency lookup", () => { }); const idem = { idempotencyKey: "klive", taskIdentifier: "t" }; try { - await buffer.accept({ runId: "live_win", envId: "env_h", orgId: "org_1", payload: "{}", ...idem }); + await buffer.accept({ + runId: "live_win", + envId: "env_h", + orgId: "org_1", + payload: "{}", + ...idem, + }); const result = await buffer.accept({ runId: "live_lose", envId: "env_h", @@ -1622,7 +1677,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -1661,7 +1716,7 @@ describe("MollifierBuffer.casSetMetadata", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1701,7 +1756,7 @@ describe("MollifierBuffer.casSetMetadata", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1742,7 +1797,7 @@ describe("MollifierBuffer.casSetMetadata", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1780,7 +1835,7 @@ describe("MollifierBuffer.casSetMetadata", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1832,7 +1887,7 @@ describe("MollifierBuffer.casSetMetadata", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -1858,7 +1913,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1902,7 +1957,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1935,7 +1990,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1990,7 +2045,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2028,7 +2083,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2061,7 +2116,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2099,7 +2154,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2130,7 +2185,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2168,7 +2223,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2200,7 +2255,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2225,7 +2280,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { const tagsToAdd = Array.from({ length: 50 }, (_, i) => `t${i}`); await Promise.all( - tagsToAdd.map((t) => buffer.mutateSnapshot("rcc", { type: "append_tags", tags: [t] })), + tagsToAdd.map((t) => buffer.mutateSnapshot("rcc", { type: "append_tags", tags: [t] })) ); const entry = await buffer.getEntry("rcc"); @@ -2234,7 +2289,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -2266,14 +2321,16 @@ describe("MollifierBuffer LIST storage", () => { // createdAtMicros lives on the entry hash (for dwell metrics) and // is plausibly recent (within the last minute, as microseconds). - const micros = Number(await buffer["redis"].hget("mollifier:entries:z1", "createdAtMicros")); + const micros = Number( + await buffer["redis"].hget("mollifier:entries:z1", "createdAtMicros") + ); const nowMicros = Date.now() * 1000; expect(micros).toBeGreaterThan(nowMicros - 60_000_000); expect(micros).toBeLessThanOrEqual(nowMicros + 1_000_000); } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2305,7 +2362,7 @@ describe("MollifierBuffer LIST storage", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2323,7 +2380,10 @@ describe("MollifierBuffer LIST storage", () => { try { await buffer.accept({ runId: "rq", envId: "env_rq", orgId: "org_1", payload: "{}" }); - const originalMicros = await buffer["redis"].hget("mollifier:entries:rq", "createdAtMicros"); + const originalMicros = await buffer["redis"].hget( + "mollifier:entries:rq", + "createdAtMicros" + ); await buffer.pop("env_rq"); // Queue is empty after the pop. @@ -2338,7 +2398,7 @@ describe("MollifierBuffer LIST storage", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -2377,25 +2437,29 @@ describe("MollifierBuffer.listEntriesForEnv", () => { } finally { await buffer.close(); } - }, + } ); - redisTest("returns empty array when env queue is empty", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "returns empty array when env queue is empty", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - expect(await buffer.listEntriesForEnv("env_empty", 10)).toEqual([]); - } finally { - await buffer.close(); + try { + expect(await buffer.listEntriesForEnv("env_empty", 10)).toEqual([]); + } finally { + await buffer.close(); + } } - }); + ); redisTest( "skips entries whose hash was torn down between LRANGE and HGETALL (concurrent drainer ack/fail race)", @@ -2429,26 +2493,30 @@ describe("MollifierBuffer.listEntriesForEnv", () => { } finally { await buffer.close(); } - }, + } ); - redisTest("maxCount <= 0 returns empty without hitting redis", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "maxCount <= 0 returns empty without hitting redis", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - expect(await buffer.listEntriesForEnv("env_a", 0)).toEqual([]); - expect(await buffer.listEntriesForEnv("env_a", -5)).toEqual([]); - } finally { - await buffer.close(); + try { + expect(await buffer.listEntriesForEnv("env_a", 0)).toEqual([]); + expect(await buffer.listEntriesForEnv("env_a", -5)).toEqual([]); + } finally { + await buffer.close(); + } } - }); + ); }); // Composite-key safety. The Redis-key builders concatenate @@ -2510,7 +2578,7 @@ describe("MollifierBuffer composite-key encoding (collision resistance)", () => } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2533,7 +2601,7 @@ describe("MollifierBuffer composite-key encoding (collision resistance)", () => // base64url alphabet has no `:`, `+`, `/`, or `=`. const afterNamespace = key.slice("mollifier:idempotency:".length); expect(afterNamespace).toMatch(/^[A-Za-z0-9_\-]+:[A-Za-z0-9_\-]+:[A-Za-z0-9_\-]+$/); - }, + } ); }); @@ -2583,7 +2651,7 @@ describe("MollifierBuffer pre-gate claim β€” ownership token safety", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2614,7 +2682,7 @@ describe("MollifierBuffer pre-gate claim β€” ownership token safety", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2657,7 +2725,7 @@ describe("MollifierBuffer pre-gate claim β€” ownership token safety", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2722,7 +2790,7 @@ describe("MollifierBuffer pre-gate claim β€” ownership token safety", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -2767,35 +2835,31 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); - redisTest( - "ack ZREMs from the draining set", - { timeout: 20_000 }, - async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest("ack ZREMs from the draining set", { timeout: 20_000 }, async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - await buffer.accept({ runId: "drn_ack", envId: "env_a", orgId: "org_1", payload: "{}" }); - await buffer.pop("env_a"); - expect(await buffer.getDrainingCount()).toBe(1); + try { + await buffer.accept({ runId: "drn_ack", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.pop("env_a"); + expect(await buffer.getDrainingCount()).toBe(1); - await buffer.ack("drn_ack"); - expect(await buffer.getDrainingCount()).toBe(0); - expect(await buffer["redis"].zscore(DRAINING_SET_KEY, "drn_ack")).toBeNull(); - } finally { - await buffer.close(); - } - }, - ); + await buffer.ack("drn_ack"); + expect(await buffer.getDrainingCount()).toBe(0); + expect(await buffer["redis"].zscore(DRAINING_SET_KEY, "drn_ack")).toBeNull(); + } finally { + await buffer.close(); + } + }); redisTest( "fail ZREMs from the draining set even though the entry hash is torn down", @@ -2820,7 +2884,7 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2850,7 +2914,7 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2888,7 +2952,7 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2928,7 +2992,7 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2969,6 +3033,6 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); }); diff --git a/packages/redis-worker/src/mollifier/buffer.ts b/packages/redis-worker/src/mollifier/buffer.ts index 66afe186a8..c61d681043 100644 --- a/packages/redis-worker/src/mollifier/buffer.ts +++ b/packages/redis-worker/src/mollifier/buffer.ts @@ -55,7 +55,7 @@ export function mollifierReconnectDelayMs( times: number, random: () => number = Math.random, stepMs: number = DEFAULT_RECONNECT_STEP_MS, - maxMs: number = DEFAULT_RECONNECT_MAX_MS, + maxMs: number = DEFAULT_RECONNECT_MAX_MS ): number { const base = Math.min(times * stepMs, maxMs); const half = Math.floor(base / 2); @@ -73,11 +73,7 @@ export type SnapshotPatch = | { type: "set_delay"; delayUntil: string } | { type: "mark_cancelled"; cancelledAt: string; cancelReason?: string }; -export type MutateSnapshotResult = - | "applied_to_snapshot" - | "not_found" - | "busy" - | "limit_exceeded"; +export type MutateSnapshotResult = "applied_to_snapshot" | "not_found" | "busy" | "limit_exceeded"; export type CasSetMetadataResult = | { kind: "applied"; newVersion: number } @@ -160,7 +156,7 @@ export class MollifierBuffer { onError: (error) => { this.logger.error("MollifierBuffer redis client error:", { error }); }, - }, + } ); this.#registerCommands(); } @@ -216,7 +212,7 @@ export class MollifierBuffer { String(createdAtMicros), "mollifier:org-envs:", idempotencyLookupKey, - "mollifier:entries:", + "mollifier:entries:" ); // Lua returns 1 (accepted), 0 (duplicate runId), or a string runId // (duplicate idempotency β€” value is the existing winner's runId). @@ -237,7 +233,7 @@ export class MollifierBuffer { DRAINING_SET_KEY, entryPrefix, envId, - "mollifier:org-envs:", + "mollifier:org-envs:" )) as string | null; if (!encoded) return null; @@ -304,11 +300,7 @@ export class MollifierBuffer { // not an error. async listEntriesForEnv(envId: string, maxCount: number): Promise { if (maxCount <= 0) return []; - const runIds = await this.redis.lrange( - `mollifier:queue:${envId}`, - 0, - maxCount - 1, - ); + const runIds = await this.redis.lrange(`mollifier:queue:${envId}`, 0, maxCount - 1); if (runIds.length === 0) return []; const pipeline = this.redis.pipeline(); @@ -356,7 +348,7 @@ export class MollifierBuffer { async mutateSnapshot(runId: string, patch: SnapshotPatch): Promise { const result = (await this.redis.mutateMollifierSnapshot( `mollifier:entries:${runId}`, - JSON.stringify(patch), + JSON.stringify(patch) )) as string; if ( result === "applied_to_snapshot" || @@ -386,7 +378,7 @@ export class MollifierBuffer { entryKey, String(input.expectedVersion), input.newMetadata, - input.newMetadataType, + input.newMetadataType )) as string; if (raw === "not_found") return { kind: "not_found" }; if (raw === "busy") return { kind: "busy" }; @@ -417,14 +409,14 @@ export class MollifierBuffer { // - "resolved": the claim already holds a runId; the caller can // return that runId as a cached hit. async claimIdempotency( - input: IdempotencyLookupInput & { token: string; ttlSeconds: number }, + input: IdempotencyLookupInput & { token: string; ttlSeconds: number } ): Promise { const claimKey = makeIdempotencyClaimKey(input); const raw = (await this.redis.claimMollifierIdempotency( claimKey, `${PENDING_PREFIX}${input.token}`, PENDING_PREFIX, - String(input.ttlSeconds), + String(input.ttlSeconds) )) as string; if (raw === "claimed") return { kind: "claimed" }; if (raw === "pending") return { kind: "pending" }; @@ -445,14 +437,14 @@ export class MollifierBuffer { // Returns true if we published; false if the claim slot was no longer // ours. async publishClaim( - input: IdempotencyLookupInput & { token: string; runId: string; ttlSeconds: number }, + input: IdempotencyLookupInput & { token: string; runId: string; ttlSeconds: number } ): Promise { const claimKey = makeIdempotencyClaimKey(input); const result = (await this.redis.publishMollifierClaim( claimKey, `${PENDING_PREFIX}${input.token}`, input.runId, - String(input.ttlSeconds), + String(input.ttlSeconds) )) as number; return result === 1; } @@ -466,10 +458,7 @@ export class MollifierBuffer { // never wiped by a slow predecessor. async releaseClaim(input: IdempotencyLookupInput & { token: string }): Promise { const claimKey = makeIdempotencyClaimKey(input); - await this.redis.releaseMollifierClaim( - claimKey, - `${PENDING_PREFIX}${input.token}`, - ); + await this.redis.releaseMollifierClaim(claimKey, `${PENDING_PREFIX}${input.token}`); } // Read the current claim value, used by the wait/poll loop on losers @@ -511,7 +500,7 @@ export class MollifierBuffer { const clearedRunId = (await this.redis.resetMollifierIdempotency( lookupKey, "mollifier:entries:", - claimKey, + claimKey )) as string; return { clearedRunId: clearedRunId.length > 0 ? clearedRunId : null }; } @@ -526,7 +515,7 @@ export class MollifierBuffer { `mollifier:entries:${runId}`, DRAINING_SET_KEY, String(this.ackGraceTtlSeconds), - runId, + runId ); } @@ -537,7 +526,7 @@ export class MollifierBuffer { DRAINING_SET_KEY, "mollifier:queue:", runId, - "mollifier:org-envs:", + "mollifier:org-envs:" ); } @@ -552,7 +541,7 @@ export class MollifierBuffer { `mollifier:entries:${runId}`, DRAINING_SET_KEY, JSON.stringify(error), - runId, + runId ); return result === 1; } @@ -579,7 +568,7 @@ export class MollifierBuffer { String(maxScore), "LIMIT", 0, - Math.max(0, limit), + Math.max(0, limit) ); } @@ -592,10 +581,9 @@ export class MollifierBuffer { return this.redis.ttl(`mollifier:entries:${runId}`); } - async evaluateTrip( envId: string, - options: { windowMs: number; threshold: number; holdMs: number }, + options: { windowMs: number; threshold: number; holdMs: number } ): Promise<{ tripped: boolean; count: number }> { const rateKey = `mollifier:rate:${envId}`; const trippedKey = `mollifier:tripped:${envId}`; @@ -604,7 +592,7 @@ export class MollifierBuffer { trippedKey, String(options.windowMs), String(options.threshold), - String(options.holdMs), + String(options.holdMs) )) as [number, number]; return { count: result[0], tripped: result[1] === 1 }; @@ -1171,7 +1159,7 @@ declare module "@internal/redis" { orgEnvsPrefix: string, idempotencyLookupKey: string, entryPrefix: string, - callback?: Callback, + callback?: Callback ): Result; popAndMarkDraining( queueKey: string, @@ -1180,7 +1168,7 @@ declare module "@internal/redis" { entryPrefix: string, envId: string, orgEnvsPrefix: string, - callback?: Callback, + callback?: Callback ): Result; requeueMollifierEntry( entryKey: string, @@ -1189,63 +1177,63 @@ declare module "@internal/redis" { queuePrefix: string, runId: string, orgEnvsPrefix: string, - callback?: Callback, + callback?: Callback ): Result; mutateMollifierSnapshot( entryKey: string, patchJson: string, - callback?: Callback, + callback?: Callback ): Result; casSetMollifierMetadata( entryKey: string, expectedVersion: string, newMetadata: string, newMetadataType: string, - callback?: Callback, + callback?: Callback ): Result; resetMollifierIdempotency( lookupKey: string, entryPrefix: string, claimKey: string, - callback?: Callback, + callback?: Callback ): Result; claimMollifierIdempotency( claimKey: string, pendingMarker: string, pendingPrefix: string, ttlSeconds: string, - callback?: Callback, + callback?: Callback ): Result; publishMollifierClaim( claimKey: string, ownerMarker: string, runId: string, ttlSeconds: string, - callback?: Callback, + callback?: Callback ): Result; releaseMollifierClaim( claimKey: string, ownerMarker: string, - callback?: Callback, + callback?: Callback ): Result; ackMollifierEntry( entryKey: string, drainingSetKey: string, graceTtlSeconds: string, runId: string, - callback?: Callback, + callback?: Callback ): Result; failMollifierEntry( entryKey: string, drainingSetKey: string, errorPayload: string, runId: string, - callback?: Callback, + callback?: Callback ): Result; delMollifierKeyIfEquals( key: string, expected: string, - callback?: Callback, + callback?: Callback ): Result; mollifierEvaluateTrip( rateKey: string, @@ -1253,7 +1241,7 @@ declare module "@internal/redis" { windowMs: string, threshold: string, holdMs: string, - callback?: Callback<[number, number]>, + callback?: Callback<[number, number]> ): Result<[number, number], Context>; } } diff --git a/packages/redis-worker/src/mollifier/drainer.test.ts b/packages/redis-worker/src/mollifier/drainer.test.ts index d4250641ee..6d42be29cb 100644 --- a/packages/redis-worker/src/mollifier/drainer.test.ts +++ b/packages/redis-worker/src/mollifier/drainer.test.ts @@ -38,96 +38,104 @@ function eachEnvAsOwnOrg(envs: string[]): Partial { } describe("MollifierDrainer.runOnce", () => { - redisTest("drains one queued entry through the handler and acks", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); - - const handlerCalls: Array<{ runId: string; envId: string; orgId: string; payload: unknown }> = - []; - const handler = async (input: { - runId: string; - envId: string; - orgId: string; - payload: unknown; - }) => { - handlerCalls.push(input); - }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 5, - maxAttempts: 3, - isRetryable: () => false, - logger: new Logger("test-drainer", "log"), - }); - - try { - await buffer.accept({ - runId: "run_1", - envId: "env_a", - orgId: "org_1", - payload: serialiseSnapshot({ foo: 1 }), + redisTest( + "drains one queued entry through the handler and acks", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, }); - const result = await drainer.runOnce(); - expect(result.drained).toBe(1); - expect(result.failed).toBe(0); - expect(handlerCalls).toHaveLength(1); - expect(handlerCalls[0]).toMatchObject({ - runId: "run_1", - envId: "env_a", - orgId: "org_1", - payload: { foo: 1 }, + const handlerCalls: Array<{ runId: string; envId: string; orgId: string; payload: unknown }> = + []; + const handler = async (input: { + runId: string; + envId: string; + orgId: string; + payload: unknown; + }) => { + handlerCalls.push(input); + }; + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 5, + maxAttempts: 3, + isRetryable: () => false, + logger: new Logger("test-drainer", "log"), }); - // After ack the entry persists as a read-fallback safety net with - // materialised=true and a fresh grace TTL. - const entry = await buffer.getEntry("run_1"); - expect(entry).not.toBeNull(); - expect(entry!.materialised).toBe(true); - } finally { - await buffer.close(); + try { + await buffer.accept({ + runId: "run_1", + envId: "env_a", + orgId: "org_1", + payload: serialiseSnapshot({ foo: 1 }), + }); + + const result = await drainer.runOnce(); + expect(result.drained).toBe(1); + expect(result.failed).toBe(0); + expect(handlerCalls).toHaveLength(1); + expect(handlerCalls[0]).toMatchObject({ + runId: "run_1", + envId: "env_a", + orgId: "org_1", + payload: { foo: 1 }, + }); + + // After ack the entry persists as a read-fallback safety net with + // materialised=true and a fresh grace TTL. + const entry = await buffer.getEntry("run_1"); + expect(entry).not.toBeNull(); + expect(entry!.materialised).toBe(true); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("runOnce with no entries does nothing", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); + redisTest( + "runOnce with no entries does nothing", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, + }); - let handlerCalls = 0; - const handler = async () => { - handlerCalls++; - }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 5, - maxAttempts: 3, - isRetryable: () => false, - logger: new Logger("test-drainer", "log"), - }); + let handlerCalls = 0; + const handler = async () => { + handlerCalls++; + }; + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 5, + maxAttempts: 3, + isRetryable: () => false, + logger: new Logger("test-drainer", "log"), + }); - try { - const result = await drainer.runOnce(); - expect(result.drained).toBe(0); - expect(result.failed).toBe(0); - expect(handlerCalls).toBe(0); - } finally { - await buffer.close(); + try { + const result = await drainer.runOnce(); + expect(result.drained).toBe(0); + expect(result.failed).toBe(0); + expect(handlerCalls).toBe(0); + } finally { + await buffer.close(); + } } - }); + ); }); describe("MollifierDrainer.drainBatchSize", () => { @@ -314,7 +322,7 @@ describe("MollifierDrainer.drainBatchSize", () => { for (let i = 0; i < envCount; i++) { queues.set( `env_${i}`, - Array.from({ length: perEnv }, (_, j) => `env_${i}_run_${j}`), + Array.from({ length: perEnv }, (_, j) => `env_${i}_run_${j}`) ); } const handled: string[] = []; @@ -379,7 +387,7 @@ describe("MollifierDrainer.drainBatchSize", () => { Array.from({ length: 100 }, (_, i) => ({ runId: `${e}_run_${i}`, orgId: "org_A", - })), + })) ); } queues.set( @@ -387,7 +395,7 @@ describe("MollifierDrainer.drainBatchSize", () => { Array.from({ length: 100 }, (_, i) => ({ runId: `${orgBEnv}_run_${i}`, orgId: "org_B", - })), + })) ); const drainedByOrg: Record = { org_A: 0, org_B: 0 }; @@ -508,7 +516,7 @@ describe("MollifierDrainer.drainBatchSize", () => { for (let i = 0; i < envCount; i++) { queues.set( `env_${i}`, - Array.from({ length: perEnv }, (_, j) => `env_${i}_run_${j}`), + Array.from({ length: perEnv }, (_, j) => `env_${i}_run_${j}`) ); } let inflightPoppedNotAcked = 0; @@ -608,95 +616,103 @@ describe("MollifierDrainer.drainBatchSize", () => { }); describe("MollifierDrainer error handling", () => { - redisTest("retryable error requeues and increments attempts", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); + redisTest( + "retryable error requeues and increments attempts", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, + }); - let calls = 0; - const handler = async () => { - calls++; - throw new Error("transient"); - }; + let calls = 0; + const handler = async () => { + calls++; + throw new Error("transient"); + }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 1, - maxAttempts: 3, - isRetryable: () => true, - logger: new Logger("test-drainer", "log"), - }); + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 1, + maxAttempts: 3, + isRetryable: () => true, + logger: new Logger("test-drainer", "log"), + }); - try { - await buffer.accept({ runId: "run_r", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "run_r", envId: "env_a", orgId: "org_1", payload: "{}" }); - await drainer.runOnce(); - const after1 = await buffer.getEntry("run_r"); - expect(after1!.status).toBe("QUEUED"); - expect(after1!.attempts).toBe(1); + await drainer.runOnce(); + const after1 = await buffer.getEntry("run_r"); + expect(after1!.status).toBe("QUEUED"); + expect(after1!.attempts).toBe(1); - await drainer.runOnce(); - const after2 = await buffer.getEntry("run_r"); - expect(after2!.status).toBe("QUEUED"); - expect(after2!.attempts).toBe(2); - - const result3 = await drainer.runOnce(); - // On attempt 3 the drainer hits maxAttempts and calls fail(), - // which deletes the entry β€” once the drainer-handler has written - // the SYSTEM_FAILURE PG row the buffer entry is no longer - // load-bearing. The runOnce result is the surviving signal. - const after3 = await buffer.getEntry("run_r"); - expect(after3).toBeNull(); - expect(result3.failed).toBe(1); - expect(calls).toBe(3); - } finally { - await buffer.close(); + await drainer.runOnce(); + const after2 = await buffer.getEntry("run_r"); + expect(after2!.status).toBe("QUEUED"); + expect(after2!.attempts).toBe(2); + + const result3 = await drainer.runOnce(); + // On attempt 3 the drainer hits maxAttempts and calls fail(), + // which deletes the entry β€” once the drainer-handler has written + // the SYSTEM_FAILURE PG row the buffer entry is no longer + // load-bearing. The runOnce result is the surviving signal. + const after3 = await buffer.getEntry("run_r"); + expect(after3).toBeNull(); + expect(result3.failed).toBe(1); + expect(calls).toBe(3); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("non-retryable error transitions directly to FAILED", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); + redisTest( + "non-retryable error transitions directly to FAILED", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, + }); - const handler = async () => { - throw new Error("validation failure"); - }; + const handler = async () => { + throw new Error("validation failure"); + }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 1, - maxAttempts: 3, - isRetryable: () => false, - logger: new Logger("test-drainer", "log"), - }); + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 1, + maxAttempts: 3, + isRetryable: () => false, + logger: new Logger("test-drainer", "log"), + }); - try { - await buffer.accept({ runId: "run_nr", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "run_nr", envId: "env_a", orgId: "org_1", payload: "{}" }); - const result = await drainer.runOnce(); + const result = await drainer.runOnce(); - // fail() deletes the entry once the drainer-handler has written - // the canonical SYSTEM_FAILURE PG row. - const entry = await buffer.getEntry("run_nr"); - expect(entry).toBeNull(); - expect(result.failed).toBe(1); - } finally { - await buffer.close(); + // fail() deletes the entry once the drainer-handler has written + // the canonical SYSTEM_FAILURE PG row. + const entry = await buffer.getEntry("run_nr"); + expect(entry).toBeNull(); + expect(result.failed).toBe(1); + } finally { + await buffer.close(); + } } - }); + ); redisTest( "multi-org round-robin: drains one item per org per runOnce", @@ -752,7 +768,7 @@ describe("MollifierDrainer error handling", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -809,7 +825,12 @@ describe("MollifierDrainer.onTerminalFailure", () => { }); try { - await buffer.accept({ runId: "run_exhaust", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.accept({ + runId: "run_exhaust", + envId: "env_a", + orgId: "org_1", + payload: "{}", + }); // Attempt 1: retryable error β†’ requeue, no terminal callback fires. const r1 = await drainer.runOnce(); @@ -836,7 +857,7 @@ describe("MollifierDrainer.onTerminalFailure", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -882,7 +903,7 @@ describe("MollifierDrainer.onTerminalFailure", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -929,7 +950,12 @@ describe("MollifierDrainer.onTerminalFailure", () => { }); try { - await buffer.accept({ runId: "run_cb_retry", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.accept({ + runId: "run_cb_retry", + envId: "env_a", + orgId: "org_1", + payload: "{}", + }); // Tick 1: handler throws β†’ attempts=1 < maxAttempts=3 β†’ requeue // (no callback invocation, retryable path). @@ -957,7 +983,7 @@ describe("MollifierDrainer.onTerminalFailure", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -992,7 +1018,12 @@ describe("MollifierDrainer.onTerminalFailure", () => { }); try { - await buffer.accept({ runId: "run_cb_dead", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.accept({ + runId: "run_cb_dead", + envId: "env_a", + orgId: "org_1", + payload: "{}", + }); const r = await drainer.runOnce(); expect(r.failed).toBe(1); @@ -1003,7 +1034,7 @@ describe("MollifierDrainer.onTerminalFailure", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1042,7 +1073,7 @@ describe("MollifierDrainer.onTerminalFailure", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -1398,17 +1429,15 @@ describe("MollifierDrainer per-tick org cap", () => { for (const h of heavy) { queues.set( h, - Array.from({ length: 100 }, (_, i) => `${h}_run_${i}`), + Array.from({ length: 100 }, (_, i) => `${h}_run_${i}`) ); } queues.set(light, [`${light}_run_0`]); - const activeEnvs = () => - [...queues.keys()].filter((k) => (queues.get(k)?.length ?? 0) > 0); + const activeEnvs = () => [...queues.keys()].filter((k) => (queues.get(k)?.length ?? 0) > 0); const buffer = makeStubBuffer({ listOrgs: async () => activeEnvs(), - listEnvsForOrg: async (orgId: string) => - activeEnvs().includes(orgId) ? [orgId] : [], + listEnvsForOrg: async (orgId: string) => (activeEnvs().includes(orgId) ? [orgId] : []), pop: async (envId: string) => { const q = queues.get(envId); if (!q || q.length === 0) return null; @@ -1472,7 +1501,7 @@ describe("MollifierDrainer per-tick org cap", () => { Array.from({ length: 100 }, (_, i) => ({ runId: `${e}_run_${i}`, orgId: "org_A", - })), + })) ); } queues.set(orgBEnv, [{ runId: `${orgBEnv}_run_0`, orgId: "org_B" }]); @@ -1563,7 +1592,7 @@ describe("MollifierDrainer per-tick org cap", () => { Array.from({ length: 100 }, (_, i) => ({ runId: `${e}_run_${i}`, orgId: "org_A", - })), + })) ); } queues.set( @@ -1571,7 +1600,7 @@ describe("MollifierDrainer per-tick org cap", () => { Array.from({ length: 100 }, (_, i) => ({ runId: `${orgBEnv}_run_${i}`, orgId: "org_B", - })), + })) ); const drainedByOrg: Record = { org_A: 0, org_B: 0 }; @@ -1648,9 +1677,7 @@ describe("MollifierDrainer per-tick org cap", () => { return anyEnvActive ? [orgId] : []; }, listEnvsForOrg: async (org: string) => - org === orgId - ? [...queues.keys()].filter((k) => (queues.get(k) ?? 0) > 0) - : [], + org === orgId ? [...queues.keys()].filter((k) => (queues.get(k) ?? 0) > 0) : [], pop: async (envId: string) => { const remaining = queues.get(envId) ?? 0; if (remaining === 0) return null; @@ -1696,7 +1723,6 @@ describe("MollifierDrainer per-tick org cap", () => { }); describe("MollifierDrainer additional coverage", () => { - it("a malformed payload is treated as a non-retryable handler error and goes terminal", async () => { // The deserialise call lives inside processEntry's try, so a JSON parse // failure is caught by the same handler-error branch. With @@ -1934,101 +1960,109 @@ describe("MollifierDrainer additional coverage", () => { }); describe("MollifierDrainer.start/stop", () => { - redisTest("start polls and processes, stop halts the loop", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); + redisTest( + "start polls and processes, stop halts the loop", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, + }); - const handled: string[] = []; - const handler = async (input: { runId: string }) => { - handled.push(input.runId); - }; + const handled: string[] = []; + const handler = async (input: { runId: string }) => { + handled.push(input.runId); + }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 5, - maxAttempts: 3, - isRetryable: () => false, - pollIntervalMs: 20, - logger: new Logger("test-drainer", "log"), - }); + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 5, + maxAttempts: 3, + isRetryable: () => false, + pollIntervalMs: 20, + logger: new Logger("test-drainer", "log"), + }); - try { - await buffer.accept({ runId: "live_1", envId: "env_a", orgId: "org_1", payload: "{}" }); - await buffer.accept({ runId: "live_2", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "live_1", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.accept({ runId: "live_2", envId: "env_a", orgId: "org_1", payload: "{}" }); - drainer.start(); + drainer.start(); - const deadline = Date.now() + 5_000; - while (handled.length < 2 && Date.now() < deadline) { - await new Promise((r) => setTimeout(r, 50)); - } + const deadline = Date.now() + 5_000; + while (handled.length < 2 && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 50)); + } - await drainer.stop(); + await drainer.stop(); - expect(new Set(handled)).toEqual(new Set(["live_1", "live_2"])); - } finally { - await buffer.close(); + expect(new Set(handled)).toEqual(new Set(["live_1", "live_2"])); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("stop returns after timeoutMs even if a handler is hung", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); + redisTest( + "stop returns after timeoutMs even if a handler is hung", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, + }); - let handlerStarted = false; - const handler = async () => { - handlerStarted = true; - await new Promise(() => {}); - }; + let handlerStarted = false; + const handler = async () => { + handlerStarted = true; + await new Promise(() => {}); + }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 1, - maxAttempts: 3, - isRetryable: () => false, - pollIntervalMs: 20, - logger: new Logger("test-drainer", "log"), - }); + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 1, + maxAttempts: 3, + isRetryable: () => false, + pollIntervalMs: 20, + logger: new Logger("test-drainer", "log"), + }); - try { - await buffer.accept({ runId: "hung", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "hung", envId: "env_a", orgId: "org_1", payload: "{}" }); - drainer.start(); + drainer.start(); - const deadline = Date.now() + 2_000; - while (!handlerStarted && Date.now() < deadline) { - await new Promise((r) => setTimeout(r, 25)); + const deadline = Date.now() + 2_000; + while (!handlerStarted && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 25)); + } + expect(handlerStarted).toBe(true); + + const stopStart = Date.now(); + await drainer.stop({ timeoutMs: 500 }); + const stopElapsed = Date.now() - stopStart; + + // Allow a small jitter window below `timeoutMs` β€” Node's setTimeout can + // fire a millisecond or two early under CI load. The behaviour we're + // pinning is "stop honors the deadline instead of waiting for the hung + // handler indefinitely", not millisecond-precise timing. + expect(stopElapsed).toBeGreaterThanOrEqual(450); + expect(stopElapsed).toBeLessThan(2_000); + } finally { + await buffer.close(); } - expect(handlerStarted).toBe(true); - - const stopStart = Date.now(); - await drainer.stop({ timeoutMs: 500 }); - const stopElapsed = Date.now() - stopStart; - - // Allow a small jitter window below `timeoutMs` β€” Node's setTimeout can - // fire a millisecond or two early under CI load. The behaviour we're - // pinning is "stop honors the deadline instead of waiting for the hung - // handler indefinitely", not millisecond-precise timing. - expect(stopElapsed).toBeGreaterThanOrEqual(450); - expect(stopElapsed).toBeLessThan(2_000); - } finally { - await buffer.close(); } - }); + ); }); describe("MollifierDrainer concurrency cap", () => { @@ -2093,6 +2127,6 @@ describe("MollifierDrainer concurrency cap", () => { } finally { await buffer.close(); } - }, + } ); }); diff --git a/packages/redis-worker/src/mollifier/drainer.ts b/packages/redis-worker/src/mollifier/drainer.ts index c831d0b56a..fc264d2421 100644 --- a/packages/redis-worker/src/mollifier/drainer.ts +++ b/packages/redis-worker/src/mollifier/drainer.ts @@ -132,9 +132,7 @@ export class MollifierDrainer { // commands into one batch, so the burst is cheap; SMEMBERS on a small set // is O(N) per org and trivial at this scale. `Promise.all` preserves // order, so the orgβ†’envs pairing below stays deterministic. - const envsByOrg = await Promise.all( - orgSlice.map((orgId) => this.buffer.listEnvsForOrg(orgId)), - ); + const envsByOrg = await Promise.all(orgSlice.map((orgId) => this.buffer.listEnvsForOrg(orgId))); const targets: string[] = []; for (let i = 0; i < orgSlice.length; i++) { const orgId = orgSlice[i]!; @@ -291,7 +289,7 @@ export class MollifierDrainer { if (winner === timeoutSentinel) { this.logger.warn( "MollifierDrainer.stop: deadline exceeded; returning while loop iteration is in flight", - { timeoutMs: options.timeoutMs }, + { timeoutMs: options.timeoutMs } ); } } finally { @@ -419,14 +417,11 @@ export class MollifierDrainer { } catch (writeErr) { if (this.isRetryable(writeErr)) { await this.buffer.requeue(entry.runId); - this.logger.warn( - "MollifierDrainer: terminal-failure callback retryable; requeued", - { - runId: entry.runId, - attempts: nextAttempts, - writeErr, - }, - ); + this.logger.warn("MollifierDrainer: terminal-failure callback retryable; requeued", { + runId: entry.runId, + attempts: nextAttempts, + writeErr, + }); return "failed"; } this.logger.error("MollifierDrainer: terminal-failure callback failed", { diff --git a/packages/redis-worker/src/mollifier/schemas.ts b/packages/redis-worker/src/mollifier/schemas.ts index 5acd0c7c15..db75ed6e45 100644 --- a/packages/redis-worker/src/mollifier/schemas.ts +++ b/packages/redis-worker/src/mollifier/schemas.ts @@ -35,7 +35,10 @@ const stringToError = z.string().transform((v, ctx) => { try { return BufferEntryError.parse(JSON.parse(v)); } catch { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "expected JSON-encoded BufferEntryError" }); + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "expected JSON-encoded BufferEntryError", + }); return z.NEVER; } }); diff --git a/packages/redis-worker/src/utils.ts b/packages/redis-worker/src/utils.ts index a577de3b0e..7e4a4cb57c 100644 --- a/packages/redis-worker/src/utils.ts +++ b/packages/redis-worker/src/utils.ts @@ -6,7 +6,5 @@ * - Native Node.js AbortError from timers/promises (sets .name) */ export function isAbortError(error: unknown): boolean { - return ( - error instanceof Error && (error.name === "AbortError" || error.message === "AbortError") - ); + return error instanceof Error && (error.name === "AbortError" || error.message === "AbortError"); } diff --git a/packages/redis-worker/src/worker.ts b/packages/redis-worker/src/worker.ts index 51812e575e..92f1451033 100644 --- a/packages/redis-worker/src/worker.ts +++ b/packages/redis-worker/src/worker.ts @@ -66,10 +66,11 @@ export type JobHandler = params: JobHandlerParams ) => Promise; -type JobHandlerFor = - Catalog[K] extends { batch: BatchConfig } - ? (items: Array>) => Promise - : (params: JobHandlerParams) => Promise; +type JobHandlerFor = Catalog[K] extends { + batch: BatchConfig; +} + ? (items: Array>) => Promise + : (params: JobHandlerParams) => Promise; export type WorkerConcurrencyOptions = { workers?: number; @@ -583,15 +584,15 @@ class Worker { await this.flushBatch(queueItem.job, workerId, limiter); } } else { - limiter(() => - this.processItem(queueItem, items.length, workerId, limiter) - ).catch((err) => { - this.logger.error("Unhandled error in processItem:", { - error: err, - workerId, - item, - }); - }); + limiter(() => this.processItem(queueItem, items.length, workerId, limiter)).catch( + (err) => { + this.logger.error("Unhandled error in processItem:", { + error: err, + workerId, + item, + }); + } + ); } } } catch (error) { @@ -785,13 +786,25 @@ class Worker { }; if (!shouldLogError) { - this.logger.info(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes); + this.logger.info( + `Worker batch item reached max attempts. Moving to DLQ.`, + dlqLogAttributes + ); } else if (errorLogLevel === "warn") { - this.logger.warn(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes); + this.logger.warn( + `Worker batch item reached max attempts. Moving to DLQ.`, + dlqLogAttributes + ); } else if (errorLogLevel === "info") { - this.logger.info(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes); + this.logger.info( + `Worker batch item reached max attempts. Moving to DLQ.`, + dlqLogAttributes + ); } else { - this.logger.error(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes); + this.logger.error( + `Worker batch item reached max attempts. Moving to DLQ.`, + dlqLogAttributes + ); } await this.queue.moveToDeadLetterQueue(item.id, errorMessage); diff --git a/packages/rsc/src/build.ts b/packages/rsc/src/build.ts index 5436bf6fb0..2b87216042 100644 --- a/packages/rsc/src/build.ts +++ b/packages/rsc/src/build.ts @@ -90,7 +90,7 @@ export function rscExtension(options?: RSCExtensionOptions): BuildExtension { build.onResolve({ filter: /^react-dom\/server$/ }, (args) => { const condition = - context.config.runtime === "bun" ? "bun" : options?.reactDomEnvironment ?? "node"; + context.config.runtime === "bun" ? "bun" : (options?.reactDomEnvironment ?? "node"); context.logger.debug("Resolving react-dom/server", { args, condition }); diff --git a/packages/schema-to-json/tsconfig.json b/packages/schema-to-json/tsconfig.json index 5bf5eba8d5..cc79a63894 100644 --- a/packages/schema-to-json/tsconfig.json +++ b/packages/schema-to-json/tsconfig.json @@ -8,4 +8,4 @@ "path": "./tsconfig.test.json" } ] -} \ No newline at end of file +} diff --git a/packages/schema-to-json/vitest.config.ts b/packages/schema-to-json/vitest.config.ts index c7da6b38e1..3f824fb954 100644 --- a/packages/schema-to-json/vitest.config.ts +++ b/packages/schema-to-json/vitest.config.ts @@ -1,8 +1,8 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, - environment: 'node', + environment: "node", }, -}); \ No newline at end of file +}); diff --git a/packages/trigger-sdk/src/v3/ai-shared.ts b/packages/trigger-sdk/src/v3/ai-shared.ts index a0ea3036cf..a77ffc2105 100644 --- a/packages/trigger-sdk/src/v3/ai-shared.ts +++ b/packages/trigger-sdk/src/v3/ai-shared.ts @@ -172,13 +172,8 @@ export type ChatInputChunk; * ``` */ -export type InferChatClientData = TTask extends Task< - string, - ChatTaskWirePayload, - any -> - ? TMetadata - : unknown; +export type InferChatClientData = + TTask extends Task, any> ? TMetadata : unknown; /** * Extracts the UI message type from a chat task (wire payload `message` items). @@ -191,13 +186,10 @@ export type InferChatClientData = TTask extends Task< * type Msg = InferChatUIMessage; * ``` */ -export type InferChatUIMessage = TTask extends Task< - string, - ChatTaskWirePayload, - any -> - ? TUIM - : UIMessage; +export type InferChatUIMessage = + TTask extends Task, any> + ? TUIM + : UIMessage; /** * Derive the chat `UIMessage` type for a given tool set. The tool-part types @@ -326,18 +318,13 @@ function isToolPartType(type: unknown): boolean { * Pairs with the per-turn merge on the agent side * (`mergeIncomingIntoHydrated` in `ai.ts`). */ -export function slimSubmitMessageForWire( - message: TMsg -): TMsg { +export function slimSubmitMessageForWire(message: TMsg): TMsg { if (!message) return message; if (message.role !== "assistant") return message; const parts = (message.parts ?? []) as any[]; const advancedToolParts = parts.filter( (p) => - p && - typeof p === "object" && - isToolPartType(p.type) && - isWireAdvanceableToolState(p.state) + p && typeof p === "object" && isToolPartType(p.type) && isWireAdvanceableToolState(p.state) ); if (advancedToolParts.length === 0) return message; const slimParts = advancedToolParts.map((p: any) => { diff --git a/packages/trigger-sdk/src/v3/ai.ts b/packages/trigger-sdk/src/v3/ai.ts index e523c9c9a8..d1286315b1 100644 --- a/packages/trigger-sdk/src/v3/ai.ts +++ b/packages/trigger-sdk/src/v3/ai.ts @@ -204,9 +204,7 @@ const lastTurnCompleteSeqNumKey = locals.create<{ value: number | undefined }>( * merge handles any dedup against snapshot-restored messages. * @internal */ -async function findLatestSessionInCursor( - chatId: string -): Promise { +async function findLatestSessionInCursor(chatId: string): Promise { const apiClient = apiClientManager.clientOrThrow(); const response = await apiClient.readSessionStreamRecords(chatId, "out"); let latestCursor: number | undefined; @@ -261,10 +259,9 @@ async function seedSessionInResumeCursorForCustomLoop( sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor); } } catch (error) { - logger.warn( - "chat session: session.in resume cursor lookup failed; old messages may replay", - { error: error instanceof Error ? error.message : String(error) } - ); + logger.warn("chat session: session.in resume cursor lookup failed; old messages may replay", { + error: error instanceof Error ? error.message : String(error), + }); } } @@ -727,9 +724,7 @@ async function replaySessionOutTail( * Not part of the public API. * @internal */ -export async function __replaySessionOutTailProductionPathForTests< - TUIMessage extends UIMessage, ->( +export async function __replaySessionOutTailProductionPathForTests( sessionId: string, options?: { lastEventId?: string } ): Promise { @@ -840,9 +835,7 @@ async function replaySessionInTail( * `__replaySessionOutTailProductionPathForTests`. * @internal */ -export async function __replaySessionInTailProductionPathForTests< - TUIMessage extends UIMessage, ->( +export async function __replaySessionInTailProductionPathForTests( sessionId: string, options?: { lastEventId?: string } ): Promise<{ message: TUIMessage; metadata: unknown; seqNum: number }[]> { @@ -903,14 +896,14 @@ function stampConversationIdOnActiveSpan( type ToolResultContent = Array< | { - type: "text"; - text: string; - } + type: "text"; + text: string; + } | { - type: "image"; - data: string; - mimeType?: string; - } + type: "image"; + data: string; + mimeType?: string; + } >; export type ToolOptions = { @@ -1094,7 +1087,9 @@ function toolFromTask< : {}), } as any); return staticTool as unknown as ToolSetCompatible< - TTaskSchema extends TaskSchema ? Tool, TOutput> : Tool + TTaskSchema extends TaskSchema + ? Tool, TOutput> + : Tool >; } @@ -1109,7 +1104,9 @@ function toolFromTask< }); return toolDefinition as unknown as ToolSetCompatible< - TTaskSchema extends TaskSchema ? Tool, TOutput> : Tool + TTaskSchema extends TaskSchema + ? Tool, TOutput> + : Tool >; } @@ -1170,7 +1167,7 @@ function getToolChatContextOrThrow(): ChatT if (!ctx) { throw new Error( "ai.chatContextOrThrow() called outside of a chat.agent context. " + - "This helper can only be used inside a subtask invoked via ai.toolExecute() (or legacy ai.tool()) from a chat.agent." + "This helper can only be used inside a subtask invoked via ai.toolExecute() (or legacy ai.tool()) from a chat.agent." ); } return ctx; @@ -1797,10 +1794,7 @@ const handoverInput = { while (true) { const result = await getChatSession().in.waitWithIdleTimeout(options); if (!result.ok) return result; - if ( - result.output.kind === "handover" || - result.output.kind === "handover-skip" - ) { + if (result.output.kind === "handover" || result.output.kind === "handover-skip") { return { ok: true as const, output: result.output as HandoverSignal }; } // Other kinds (message, stop) are not expected during handover-prepare. @@ -2017,11 +2011,11 @@ const chatUIStreamPerTurnKey = locals.create>( - "chat.toolCallToMessageId" -); +const chatToolCallToMessageIdKey = locals.create>("chat.toolCallToMessageId"); -function recordToolCallIdsFromMessage(message: { id?: string; role?: string; parts?: unknown[] } | undefined) { +function recordToolCallIdsFromMessage( + message: { id?: string; role?: string; parts?: unknown[] } | undefined +) { if (!message || message.role !== "assistant" || !message.id) return; let map = locals.get(chatToolCallToMessageIdKey); if (!map) { @@ -2291,9 +2285,7 @@ function extractNewToolResultsFromHistory( message: UIMessage, messages: UIMessage[] ): ChatNewToolResult[] { - const resolved = new Set( - getResolvedToolCallsFromHistory(messages).map((r) => r.toolCallId) - ); + const resolved = new Set(getResolvedToolCallsFromHistory(messages).map((r) => r.toolCallId)); const seen = new Set(); const out: ChatNewToolResult[] = []; for (const { part, toolCallId, toolName, state } of iterateToolParts(message)) { @@ -2811,9 +2803,9 @@ export type PrepareMessagesEvent = { messages: ModelMessage[]; /** Why messages are being prepared. */ reason: - | "run" // Messages being passed to run() for streamText - | "compaction-rebuild" // Rebuilding from a previous compaction summary - | "compaction-result"; // Fresh compaction just produced these messages + | "run" // Messages being passed to run() for streamText + | "compaction-rebuild" // Rebuilding from a previous compaction summary + | "compaction-result"; // Fresh compaction just produced these messages /** The chat session ID. */ chatId: string; /** The current turn number (0-indexed). */ @@ -3090,9 +3082,12 @@ async function applyPrepareMessages( * declared. * @internal */ -async function resolveTurnTools( - override?: { chatId: string; turn: number; continuation: boolean; clientData: unknown } -): Promise { +async function resolveTurnTools(override?: { + chatId: string; + turn: number; + continuation: boolean; + clientData: unknown; +}): Promise { const option = locals.get(chatToolsOptionKey); if (!option) return; @@ -3193,18 +3188,18 @@ async function chatCompact( const shouldTrigger = options.shouldCompact ? await options.shouldCompact({ - messages, - totalTokens, - inputTokens, - outputTokens, - usage: currentStep.usage, - source: "inner", - stepNumber, - steps, - chatId: turnCtx?.chatId, - turn: turnCtx?.turn, - clientData: turnCtx?.clientData, - }) + messages, + totalTokens, + inputTokens, + outputTokens, + usage: currentStep.usage, + source: "inner", + stepNumber, + steps, + chatId: turnCtx?.chatId, + turn: turnCtx?.turn, + clientData: turnCtx?.clientData, + }) : totalTokens != null && options.threshold != null && totalTokens > options.threshold; if (!shouldTrigger) { @@ -3520,16 +3515,16 @@ function isCompactionSafe(messages: UIMessage[]): boolean { export type ChatPromptValue = | ResolvedPrompt | { - text: string; - model: undefined; - config: undefined; - promptId: string; - version: number; - labels: string[]; - toAISDKTelemetry: (additionalMetadata?: Record) => { - experimental_telemetry: { isEnabled: true; metadata: Record }; + text: string; + model: undefined; + config: undefined; + promptId: string; + version: number; + labels: string[]; + toAISDKTelemetry: (additionalMetadata?: Record) => { + experimental_telemetry: { isEnabled: true; metadata: Record }; + }; }; - }; /** @internal */ const chatPromptKey = locals.create("chat.prompt"); @@ -3629,9 +3624,7 @@ function getChatSkills(): ResolvedSkill[] | undefined { */ function buildSkillsSystemPrompt(skills: ResolvedSkill[]): string { if (skills.length === 0) return ""; - const lines = skills.map( - (s) => `- ${s.frontmatter.name}: ${s.frontmatter.description}` - ); + const lines = skills.map((s) => `- ${s.frontmatter.name}: ${s.frontmatter.description}`); return [ "Available skills (call `loadSkill` to read the full instructions before using one):", ...lines, @@ -3692,7 +3685,8 @@ export function buildSkillTools(skills: ResolvedSkill[]): Record { skill: { type: "string", description: "The skill's name (from frontmatter)." }, path: { type: "string", - description: "Relative path inside the skill folder (e.g. `references/citation-style.md`).", + description: + "Relative path inside the skill folder (e.g. `references/citation-style.md`).", }, }, required: ["skill", "path"], @@ -3723,7 +3717,8 @@ export function buildSkillTools(skills: ResolvedSkill[]): Record { skill: { type: "string", description: "The skill's name (from frontmatter)." }, command: { type: "string", - description: "Bash command to run. Relative script paths resolve against the skill's root.", + description: + "Bash command to run. Relative script paths resolve against the skill's root.", }, }, required: ["skill", "command"], @@ -4079,7 +4074,7 @@ async function pipeChat( } else { throw new Error( "pipeChat: source must be a StreamTextResult (with .toUIMessageStream()), " + - "an AsyncIterable, or a ReadableStream" + "an AsyncIterable, or a ReadableStream" ); } @@ -4544,53 +4539,53 @@ export type BeforeTurnCompleteEvent< */ export type ChatSuspendEvent = | { - /** Suspend is happening after onPreload, before the first message. */ - phase: "preload"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** Custom data from the frontend. */ - clientData?: TClientData; - } + /** Suspend is happening after onPreload, before the first message. */ + phase: "preload"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** Custom data from the frontend. */ + clientData?: TClientData; + } | { - /** - * Suspend is happening on a continuation run that booted with no incoming - * message (post-`endRun`, post-waitpoint-timeout, etc.) and is waiting - * for the next session.in record before running any turn. Distinct from - * `phase: "preload"` β€” the chat already started; `onPreload` has not - * fired and will not fire on this run. - */ - phase: "continuation"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** Custom data from the frontend. */ - clientData?: TClientData; - } + /** + * Suspend is happening on a continuation run that booted with no incoming + * message (post-`endRun`, post-waitpoint-timeout, etc.) and is waiting + * for the next session.in record before running any turn. Distinct from + * `phase: "preload"` β€” the chat already started; `onPreload` has not + * fired and will not fire on this run. + */ + phase: "continuation"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** Custom data from the frontend. */ + clientData?: TClientData; + } | { - /** Suspend is happening after a completed turn, waiting for the next message. */ - phase: "turn"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** The turn number (0-indexed) that just completed. */ - turn: number; - /** The accumulated model messages after the completed turn. */ - messages: ModelMessage[]; - /** The accumulated UI messages after the completed turn. */ - uiMessages: TUIM[]; - /** Custom data from the frontend. */ - clientData?: TClientData; - }; + /** Suspend is happening after a completed turn, waiting for the next message. */ + phase: "turn"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** The turn number (0-indexed) that just completed. */ + turn: number; + /** The accumulated model messages after the completed turn. */ + messages: ModelMessage[]; + /** The accumulated UI messages after the completed turn. */ + uiMessages: TUIM[]; + /** Custom data from the frontend. */ + clientData?: TClientData; + }; /** * Discriminated event passed to the `onChatResume` callback. @@ -4598,52 +4593,52 @@ export type ChatSuspendEvent = | { - /** First message arrived after preload suspension. */ - phase: "preload"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** Custom data from the frontend. */ - clientData?: TClientData; - } + /** First message arrived after preload suspension. */ + phase: "preload"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** Custom data from the frontend. */ + clientData?: TClientData; + } | { - /** - * First message arrived after continuation-wait suspension. Distinct - * from `phase: "preload"` β€” the chat already started; this is a new - * run picking up after a prior run ended (`endRun`, waitpoint timeout, - * etc.). - */ - phase: "continuation"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** Custom data from the frontend. */ - clientData?: TClientData; - } + /** + * First message arrived after continuation-wait suspension. Distinct + * from `phase: "preload"` β€” the chat already started; this is a new + * run picking up after a prior run ended (`endRun`, waitpoint timeout, + * etc.). + */ + phase: "continuation"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** Custom data from the frontend. */ + clientData?: TClientData; + } | { - /** Next message arrived after turn suspension. */ - phase: "turn"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** The turn number that was completed before suspension. */ - turn: number; - /** The accumulated model messages (from before suspension). */ - messages: ModelMessage[]; - /** The accumulated UI messages (from before suspension). */ - uiMessages: TUIM[]; - /** Custom data from the frontend. */ - clientData?: TClientData; - }; + /** Next message arrived after turn suspension. */ + phase: "turn"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** The turn number that was completed before suspension. */ + turn: number; + /** The accumulated model messages (from before suspension). */ + messages: ModelMessage[]; + /** The accumulated UI messages (from before suspension). */ + uiMessages: TUIM[]; + /** Custom data from the frontend. */ + clientData?: TClientData; + }; export type ChatAgentOptions< TIdentifier extends string, @@ -4806,9 +4801,7 @@ export type ChatAgentOptions< * **Auto-piping:** If this function returns a value with `.toUIMessageStream()`, * the stream is automatically piped to the frontend. */ - run: ( - payload: ChatTaskRunPayload, TTools> - ) => Promise; + run: (payload: ChatTaskRunPayload, TTools>) => Promise; /** * Called once at the start of every run boot β€” for the initial run, for @@ -5573,12 +5566,9 @@ function chatAgent< // pump kicks in. Populated by `onRecoveryBoot.recoveredTurns` (or its // default, `inFlightUsers`). The turn-loop checks this queue ahead of // `messagesInput.waitWithIdleTimeout` so recovered turns fire first. - const bootInjectedQueue: ChatTaskWirePayload< - TUIMessage, - inferSchemaIn - >[] = []; - const couldHavePriorState = - payload.continuation === true || ctx.attempt.number > 1; + const bootInjectedQueue: ChatTaskWirePayload>[] = + []; + const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1; // `.in` resume cursor, computed at most once per boot. The boot // block below resolves it (snapshot field or records scan) and the @@ -5600,13 +5590,10 @@ function chatAgent< } catch (error) { // `readChatSnapshot` already swallows + warns internally; this catch // is just belt-and-suspenders against tracer/span errors. - logger.warn( - "chat.agent: snapshot read failed; continuing without snapshot", - { - error: error instanceof Error ? error.message : String(error), - sessionId: sessionIdForSnapshot, - } - ); + logger.warn("chat.agent: snapshot read failed; continuing without snapshot", { + error: error instanceof Error ? error.message : String(error), + sessionId: sessionIdForSnapshot, + }); } bootSpan.setAttribute("chat.boot.snapshot.durationMs", Date.now() - snapStart); bootSpan.setAttribute("chat.boot.snapshot.present", !!bootSnapshot); @@ -5636,26 +5623,19 @@ function chatAgent< const replayOutPhase = async () => { const replayOutStart = Date.now(); try { - const replayResult = await replaySessionOutTail( - sessionIdForSnapshot, - { lastEventId: bootSnapshot?.lastOutEventId } - ); + const replayResult = await replaySessionOutTail(sessionIdForSnapshot, { + lastEventId: bootSnapshot?.lastOutEventId, + }); replayedSettled = replayResult.settled; replayedPartial = replayResult.partial; replayedPartialRaw = replayResult.partialRaw; } catch (error) { - logger.warn( - "chat.agent: session.out replay failed; using snapshot only", - { - error: error instanceof Error ? error.message : String(error), - sessionId: sessionIdForSnapshot, - } - ); + logger.warn("chat.agent: session.out replay failed; using snapshot only", { + error: error instanceof Error ? error.message : String(error), + sessionId: sessionIdForSnapshot, + }); } - bootSpan.setAttribute( - "chat.boot.replay.out.durationMs", - Date.now() - replayOutStart - ); + bootSpan.setAttribute("chat.boot.replay.out.durationMs", Date.now() - replayOutStart); bootSpan.setAttribute("chat.boot.replay.out.settledCount", replayedSettled.length); bootSpan.setAttribute( "chat.boot.replay.out.partialPresent", @@ -5710,14 +5690,8 @@ function chatAgent< { error: error instanceof Error ? error.message : String(error) } ); } - bootSpan.setAttribute( - "chat.boot.replay.in.durationMs", - Date.now() - replayInStart - ); - bootSpan.setAttribute( - "chat.boot.replay.in.userCount", - replayedInTail.length - ); + bootSpan.setAttribute("chat.boot.replay.in.durationMs", Date.now() - replayInStart); + bootSpan.setAttribute("chat.boot.replay.in.userCount", replayedInTail.length); }; await Promise.all([replayOutPhase(), replayInPhase()]); @@ -5760,9 +5734,7 @@ function chatAgent< // - Snapshot exists at all (catches edge cases where the wire // didn't set `continuation` but a snapshot indicates prior turns) const needsResumeCursor = - ctx.attempt.number > 1 || - payload.continuation === true || - bootSnapshot !== undefined; + ctx.attempt.number > 1 || payload.continuation === true || bootSnapshot !== undefined; if (needsResumeCursor) { try { @@ -5854,8 +5826,7 @@ function chatAgent< if (Array.isArray(hookResult.chain)) hookChain = hookResult.chain; if (Array.isArray(hookResult.recoveredTurns)) hookRecoveredTurns = hookResult.recoveredTurns; - if (typeof hookResult.beforeBoot === "function") - hookBeforeBoot = hookResult.beforeBoot; + if (typeof hookResult.beforeBoot === "function") hookBeforeBoot = hookResult.beforeBoot; } } @@ -5912,8 +5883,7 @@ function chatAgent< // across attempts, but session.in records it once), the wire // payload already runs turn 0 β€” drop the duplicate from the queue // so we don't fire the same turn twice. - const wireMessageId = - (payload.message as { id?: string } | undefined)?.id; + const wireMessageId = (payload.message as { id?: string } | undefined)?.id; const metadataById = new Map(); for (const entry of replayedInTail) { metadataById.set(entry.message.id, entry.metadata); @@ -5993,7 +5963,6 @@ function chatAgent< // Make the seeded UI accumulator visible to `chat.history.*` // before any hook (`onChatStart`, `onTurnStart`, etc.) fires. locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); - } // Token usage tracking across turns @@ -6148,9 +6117,7 @@ function chatAgent< // `transport.preload(..., { idleTimeoutInSeconds })` / wire payload so // `chat.agent({ idleTimeoutInSeconds, preloadIdleTimeoutInSeconds })` is authoritative. const effectivePreloadIdleTimeout = - preloadIdleTimeoutInSeconds ?? - idleTimeoutInSeconds ?? - payload.idleTimeoutInSeconds; + preloadIdleTimeoutInSeconds ?? idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds; const effectivePreloadTimeout = (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? @@ -6164,51 +6131,51 @@ function chatAgent< skipSuspend: exitAfterPreloadIdle, onSuspend: onChatSuspend ? async () => { - await tracer.startActiveSpan( - "onChatSuspend()", - async () => { - await onChatSuspend({ - phase: "preload", - ctx, - chatId: payload.chatId, - runId: currentRunId, - clientData: preloadClientData, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": payload.chatId, - "chat.suspend.phase": "preload", + await tracer.startActiveSpan( + "onChatSuspend()", + async () => { + await onChatSuspend({ + phase: "preload", + ctx, + chatId: payload.chatId, + runId: currentRunId, + clientData: preloadClientData, + }); }, - } - ); - } + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": payload.chatId, + "chat.suspend.phase": "preload", + }, + } + ); + } : undefined, onResume: onChatResume ? async () => { - await tracer.startActiveSpan( - "onChatResume()", - async () => { - await onChatResume({ - phase: "preload", - ctx, - chatId: payload.chatId, - runId: currentRunId, - clientData: preloadClientData, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": payload.chatId, - "chat.resume.phase": "preload", + await tracer.startActiveSpan( + "onChatResume()", + async () => { + await onChatResume({ + phase: "preload", + ctx, + chatId: payload.chatId, + runId: currentRunId, + clientData: preloadClientData, + }); }, - } - ); - } + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": payload.chatId, + "chat.resume.phase": "preload", + }, + } + ); + } : undefined, }); @@ -6279,10 +6246,7 @@ function chatAgent< // call entirely (the response is already complete). // `onTurnComplete` fires with the partial as // `responseMessage` so persistence works normally. - locals.set( - chatHandoverPartialKey, - handoverResult.output.partialAssistantMessage - ); + locals.set(chatHandoverPartialKey, handoverResult.output.partialAssistantMessage); // Stash the customer-side step-1 messageId. Turn-0 setup // uses it to seed the synthesized partial UIMessage with the // SAME id, so the agent's post-handover chunks merge into @@ -6338,341 +6302,340 @@ function chatAgent< if (bootInjectedQueue.length > 0) { currentWirePayload = bootInjectedQueue.shift()!; } else { - - const effectiveIdleTimeout = - idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds; - const effectiveTurnTimeout = - (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; - - const continuationResult = await messagesInput.waitWithIdleTimeout({ - idleTimeoutInSeconds: effectiveIdleTimeout, - timeout: effectiveTurnTimeout, - spanName: "waiting for first message (continuation)", - onSuspend: onChatSuspend - ? async () => { - await tracer.startActiveSpan( - "onChatSuspend()", - async () => { - await onChatSuspend({ - phase: "continuation", - ctx, - chatId: payload.chatId, - runId: ctx.run.id, - clientData: continuationClientData, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": payload.chatId, - "chat.suspend.phase": "continuation", + const effectiveIdleTimeout = idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds; + const effectiveTurnTimeout = + (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; + + const continuationResult = await messagesInput.waitWithIdleTimeout({ + idleTimeoutInSeconds: effectiveIdleTimeout, + timeout: effectiveTurnTimeout, + spanName: "waiting for first message (continuation)", + onSuspend: onChatSuspend + ? async () => { + await tracer.startActiveSpan( + "onChatSuspend()", + async () => { + await onChatSuspend({ + phase: "continuation", + ctx, + chatId: payload.chatId, + runId: ctx.run.id, + clientData: continuationClientData, + }); }, - } - ); - } - : undefined, - onResume: onChatResume - ? async () => { - await tracer.startActiveSpan( - "onChatResume()", - async () => { - await onChatResume({ - phase: "continuation", - ctx, - chatId: payload.chatId, - runId: ctx.run.id, - clientData: continuationClientData, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": payload.chatId, - "chat.resume.phase": "continuation", + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": payload.chatId, + "chat.suspend.phase": "continuation", + }, + } + ); + } + : undefined, + onResume: onChatResume + ? async () => { + await tracer.startActiveSpan( + "onChatResume()", + async () => { + await onChatResume({ + phase: "continuation", + ctx, + chatId: payload.chatId, + runId: ctx.run.id, + clientData: continuationClientData, + }); }, - } - ); - } - : undefined, - }); + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": payload.chatId, + "chat.resume.phase": "continuation", + }, + } + ); + } + : undefined, + }); - if (!continuationResult.ok) { - // Timed out waiting for the customer's next message β€” exit. - return; - } + if (!continuationResult.ok) { + // Timed out waiting for the customer's next message β€” exit. + return; + } - currentWirePayload = continuationResult.output as ChatTaskWirePayload< - TUIMessage, - inferSchemaIn - >; + currentWirePayload = continuationResult.output as ChatTaskWirePayload< + TUIMessage, + inferSchemaIn + >; - if (currentWirePayload.trigger === "close") { - return; - } + if (currentWirePayload.trigger === "close") { + return; + } } // end else (no boot-injected first turn) } for (let turn = 0; turn < maxTurns; turn++) { try { - // Extract turn-level context before entering the span. Slim - // wire: at most one delta message per record. `headStartMessages` - // is consumed at boot only (via `payload.headStartMessages`) - // and intentionally discarded here. - const { - metadata: wireMetadata, - message: incomingMessage, - headStartMessages: _hsm, - ...restWire - } = currentWirePayload; - void _hsm; - const incomingMessages: TUIMessage[] = incomingMessage - ? [incomingMessage as TUIMessage] - : []; - // Cleaning happens once here so `extractLastUserMessageText` and - // every downstream consumer see the same message shape β€” and - // `cleanupAbortedParts` no longer has to be re-applied below. - const cleanedIncomingMessages: TUIMessage[] = incomingMessages.map((msg) => - msg.role === "assistant" ? cleanupAbortedParts(msg) : msg - ); - const clientData = ( - parseClientData ? await parseClientData(wireMetadata) : wireMetadata - ) as inferSchemaOut; - const lastUserMessage = extractLastUserMessageText(cleanedIncomingMessages); - - // Actions are not turns. They use a different span name - // and don't carry a turn.number. Branched on at `isAction`. - const isAction = currentWirePayload.trigger === "action"; - const spanName = isAction ? "chat action" : `chat turn ${turn + 1}`; - - const turnAttributes: Attributes = { - ...(isAction ? {} : { "turn.number": turn + 1 }), - "gen_ai.conversation.id": currentWirePayload.chatId, - "gen_ai.operation.name": "chat", - "chat.trigger": currentWirePayload.trigger, - [SemanticInternalAttributes.STYLE_ICON]: isAction - ? "tabler-bolt" - : "tabler-message-chatbot", - [SemanticInternalAttributes.ENTITY_TYPE]: isAction ? "chat-action" : "chat-turn", - }; - - if (lastUserMessage) { - turnAttributes["chat.user_message"] = lastUserMessage; - - // Show a truncated preview of the user message as an accessory - const preview = - lastUserMessage.length > 80 ? lastUserMessage.slice(0, 80) + "..." : lastUserMessage; - Object.assign( - turnAttributes, - accessoryAttributes({ - items: [{ text: preview, variant: "normal" }], - style: "codepath", - }) - ); - } - - if (wireMetadata !== undefined) { - turnAttributes["chat.client_data"] = - typeof wireMetadata === "string" ? wireMetadata : JSON.stringify(wireMetadata); - } + // Extract turn-level context before entering the span. Slim + // wire: at most one delta message per record. `headStartMessages` + // is consumed at boot only (via `payload.headStartMessages`) + // and intentionally discarded here. + const { + metadata: wireMetadata, + message: incomingMessage, + headStartMessages: _hsm, + ...restWire + } = currentWirePayload; + void _hsm; + const incomingMessages: TUIMessage[] = incomingMessage + ? [incomingMessage as TUIMessage] + : []; + // Cleaning happens once here so `extractLastUserMessageText` and + // every downstream consumer see the same message shape β€” and + // `cleanupAbortedParts` no longer has to be re-applied below. + const cleanedIncomingMessages: TUIMessage[] = incomingMessages.map((msg) => + msg.role === "assistant" ? cleanupAbortedParts(msg) : msg + ); + const clientData = ( + parseClientData ? await parseClientData(wireMetadata) : wireMetadata + ) as inferSchemaOut; + const lastUserMessage = extractLastUserMessageText(cleanedIncomingMessages); + + // Actions are not turns. They use a different span name + // and don't carry a turn.number. Branched on at `isAction`. + const isAction = currentWirePayload.trigger === "action"; + const spanName = isAction ? "chat action" : `chat turn ${turn + 1}`; + + const turnAttributes: Attributes = { + ...(isAction ? {} : { "turn.number": turn + 1 }), + "gen_ai.conversation.id": currentWirePayload.chatId, + "gen_ai.operation.name": "chat", + "chat.trigger": currentWirePayload.trigger, + [SemanticInternalAttributes.STYLE_ICON]: isAction + ? "tabler-bolt" + : "tabler-message-chatbot", + [SemanticInternalAttributes.ENTITY_TYPE]: isAction ? "chat-action" : "chat-turn", + }; - const turnResult = await tracer.startActiveSpan( - spanName, - async (turnSpan) => { - // (errors are caught by the outer try/catch which writes an error chunk) - locals.set(chatPipeCountKey, 0); - locals.set(chatDeferKey, new Set()); - locals.set(chatCompactionStateKey, undefined); - locals.set(chatSteeringQueueKey, []); - locals.set(chatResponsePartsKey, []); - // NOTE: chatBackgroundQueueKey is NOT reset here β€” messages injected - // by deferred work from the previous turn's onTurnComplete need to - // survive into the next turn. The queue is drained before run(). - locals.set(chatInjectedMessageIdsKey, new Set()); + if (lastUserMessage) { + turnAttributes["chat.user_message"] = lastUserMessage; + + // Show a truncated preview of the user message as an accessory + const preview = + lastUserMessage.length > 80 + ? lastUserMessage.slice(0, 80) + "..." + : lastUserMessage; + Object.assign( + turnAttributes, + accessoryAttributes({ + items: [{ text: preview, variant: "normal" }], + style: "codepath", + }) + ); + } - // Store chat context for auto-detection by task-tool subtasks (ai.toolExecute / legacy ai.tool) - locals.set(chatTurnContextKey, { - chatId: currentWirePayload.chatId, - turn, - continuation, - clientData, - }); + if (wireMetadata !== undefined) { + turnAttributes["chat.client_data"] = + typeof wireMetadata === "string" ? wireMetadata : JSON.stringify(wireMetadata); + } - // Resolve the per-turn `tools` set now that turn context - // (incl. parsed clientData) exists, so every toModelMessages - // call this turn can re-apply tool `toModelOutput`. - await resolveTurnTools(); - - // Per-turn stop controller (reset each turn) - const stopController = new AbortController(); - currentStopController = stopController; - locals.set(chatStopControllerKey, stopController); - - // Three signals for the user's run function - const stopSignal = stopController.signal; - const cancelSignal = runSignal; - const combinedSignal = AbortSignal.any([runSignal, stopController.signal]); - - // Buffer messages that arrive during streaming - const pendingMessages: ChatTaskWirePayload< - TUIMessage, - inferSchemaIn - >[] = []; - const pmConfig = locals.get(chatPendingMessagesKey); - const msgSub = messagesInput.on(async (msg) => { - // If pendingMessages is configured, route to the steering queue - // instead of the wire buffer. The frontend handles re-sending - // non-injected messages via sendMessage on turn complete. - if (pmConfig) { - // Slim wire: at most one delta message per record. The - // pendingMessages handler reads `msg.message` directly - // instead of slicing an array β€” a wire record arrives - // with the new user message in `.message`, or no message - // at all (regenerate / preload / close / handover-prepare). - const lastUIMessage = msg.message as TUIMessage | undefined; - if (lastUIMessage) { - if (pmConfig.onReceived) { - try { - await pmConfig.onReceived({ - message: lastUIMessage as TUIMessage, - chatId: currentWirePayload.chatId, - turn, - }); - } catch { - /* non-fatal */ - } - } + const turnResult = await tracer.startActiveSpan( + spanName, + async (turnSpan) => { + // (errors are caught by the outer try/catch which writes an error chunk) + locals.set(chatPipeCountKey, 0); + locals.set(chatDeferKey, new Set()); + locals.set(chatCompactionStateKey, undefined); + locals.set(chatSteeringQueueKey, []); + locals.set(chatResponsePartsKey, []); + // NOTE: chatBackgroundQueueKey is NOT reset here β€” messages injected + // by deferred work from the previous turn's onTurnComplete need to + // survive into the next turn. The queue is drained before run(). + locals.set(chatInjectedMessageIdsKey, new Set()); + + // Store chat context for auto-detection by task-tool subtasks (ai.toolExecute / legacy ai.tool) + locals.set(chatTurnContextKey, { + chatId: currentWirePayload.chatId, + turn, + continuation, + clientData, + }); + // Resolve the per-turn `tools` set now that turn context + // (incl. parsed clientData) exists, so every toModelMessages + // call this turn can re-apply tool `toModelOutput`. + await resolveTurnTools(); + + // Per-turn stop controller (reset each turn) + const stopController = new AbortController(); + currentStopController = stopController; + locals.set(chatStopControllerKey, stopController); + + // Three signals for the user's run function + const stopSignal = stopController.signal; + const cancelSignal = runSignal; + const combinedSignal = AbortSignal.any([runSignal, stopController.signal]); + + // Buffer messages that arrive during streaming + const pendingMessages: ChatTaskWirePayload< + TUIMessage, + inferSchemaIn + >[] = []; + const pmConfig = locals.get(chatPendingMessagesKey); + const msgSub = messagesInput.on(async (msg) => { + // If pendingMessages is configured, route to the steering queue + // instead of the wire buffer. The frontend handles re-sending + // non-injected messages via sendMessage on turn complete. + if (pmConfig) { + // Slim wire: at most one delta message per record. The + // pendingMessages handler reads `msg.message` directly + // instead of slicing an array β€” a wire record arrives + // with the new user message in `.message`, or no message + // at all (regenerate / preload / close / handover-prepare). + const lastUIMessage = msg.message as TUIMessage | undefined; + if (lastUIMessage) { + if (pmConfig.onReceived) { try { - const queue = locals.get(chatSteeringQueueKey) ?? []; - // Deduplicate by message ID β€” guards against double-sends - if ( - lastUIMessage.id && - queue.some((e) => e.uiMessage.id === lastUIMessage.id) - ) { - return; - } - const modelMsgs = await toModelMessages([lastUIMessage]); - queue.push({ - uiMessage: lastUIMessage as UIMessage, - modelMessages: modelMsgs, + await pmConfig.onReceived({ + message: lastUIMessage as TUIMessage, + chatId: currentWirePayload.chatId, + turn, }); - locals.set(chatSteeringQueueKey, queue); } catch { - /* conversion failed β€” skip steering queue */ + /* non-fatal */ } } - return; // Don't add to wire buffer β€” frontend handles non-injected case - } - // No pendingMessages config β€” standard wire buffer for next turn - pendingMessages.push( - msg as ChatTaskWirePayload> - ); - }); - - // Track new messages for this turn (user input + assistant response). - const turnNewModelMessages: ModelMessage[] = []; - const turnNewUIMessages: TUIMessage[] = []; - - // ── Action handling ────────────────────────────────────── - // Actions arrive on the same input stream but with - // trigger === "action". They are NOT turns β€” only - // `hydrateMessages` and `onAction` fire. No turn lifecycle - // hooks (`onTurnStart` / `prepareMessages` / - // `onBeforeTurnComplete` / `onTurnComplete`) and no - // `run()` invocation. To produce a model response from - // an action, return a `StreamTextResult` (auto-piped), - // string, or UIMessage from `onAction`. Turn counter - // does not advance. - let actionStreamResult: unknown = undefined; - if (isAction) { - // Parse and validate the action payload - const parsedAction = parseAction - ? await parseAction(currentWirePayload.action) - : currentWirePayload.action; - - // Hydrate messages from backend if configured - if (hydrateMessages) { - const hydrated = await tracer.startActiveSpan( - "hydrateMessages()", - async () => { - return hydrateMessages({ - chatId: currentWirePayload.chatId, - turn, - trigger: "action", - incomingMessages: [] as TUIMessage[], - previousMessages: [...accumulatedUIMessages], - clientData, - continuation, - previousRunId, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": currentWirePayload.chatId, - "chat.trigger": "action", - }, + try { + const queue = locals.get(chatSteeringQueueKey) ?? []; + // Deduplicate by message ID β€” guards against double-sends + if ( + lastUIMessage.id && + queue.some((e) => e.uiMessage.id === lastUIMessage.id) + ) { + return; } - ); - accumulatedUIMessages = [...hydrated] as TUIMessage[]; - accumulatedMessages = await toModelMessages(hydrated); - locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); + const modelMsgs = await toModelMessages([lastUIMessage]); + queue.push({ + uiMessage: lastUIMessage as UIMessage, + modelMessages: modelMsgs, + }); + locals.set(chatSteeringQueueKey, queue); + } catch { + /* conversion failed β€” skip steering queue */ + } } + return; // Don't add to wire buffer β€” frontend handles non-injected case + } - // Fire onAction β€” handler may mutate state via - // `chat.history.*` and / or return a model response. - if (onAction) { - actionStreamResult = await tracer.startActiveSpan( - "onAction()", - async () => { - return await onAction({ - action: parsedAction as any, - chatId: currentWirePayload.chatId, - turn, - clientData, - uiMessages: accumulatedUIMessages, - messages: accumulatedMessages, - }); + // No pendingMessages config β€” standard wire buffer for next turn + pendingMessages.push( + msg as ChatTaskWirePayload> + ); + }); + + // Track new messages for this turn (user input + assistant response). + const turnNewModelMessages: ModelMessage[] = []; + const turnNewUIMessages: TUIMessage[] = []; + + // ── Action handling ────────────────────────────────────── + // Actions arrive on the same input stream but with + // trigger === "action". They are NOT turns β€” only + // `hydrateMessages` and `onAction` fire. No turn lifecycle + // hooks (`onTurnStart` / `prepareMessages` / + // `onBeforeTurnComplete` / `onTurnComplete`) and no + // `run()` invocation. To produce a model response from + // an action, return a `StreamTextResult` (auto-piped), + // string, or UIMessage from `onAction`. Turn counter + // does not advance. + let actionStreamResult: unknown = undefined; + if (isAction) { + // Parse and validate the action payload + const parsedAction = parseAction + ? await parseAction(currentWirePayload.action) + : currentWirePayload.action; + + // Hydrate messages from backend if configured + if (hydrateMessages) { + const hydrated = await tracer.startActiveSpan( + "hydrateMessages()", + async () => { + return hydrateMessages({ + chatId: currentWirePayload.chatId, + turn, + trigger: "action", + incomingMessages: [] as TUIMessage[], + previousMessages: [...accumulatedUIMessages], + clientData, + continuation, + previousRunId, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": currentWirePayload.chatId, + "chat.trigger": "action", }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": currentWirePayload.chatId, - "chat.action": - typeof parsedAction === "object" && parsedAction !== null - ? JSON.stringify(parsedAction) - : String(parsedAction), - }, - } - ); + } + ); + accumulatedUIMessages = [...hydrated] as TUIMessage[]; + accumulatedMessages = await toModelMessages(hydrated); + locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); + } - // Apply chat.history mutations from onAction - const actionOverride = locals.get(chatOverrideMessagesKey); - if (actionOverride) { - locals.set(chatOverrideMessagesKey, undefined); - accumulatedUIMessages = [...actionOverride] as TUIMessage[]; - accumulatedMessages = await toModelMessages(actionOverride); - locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); + // Fire onAction β€” handler may mutate state via + // `chat.history.*` and / or return a model response. + if (onAction) { + actionStreamResult = await tracer.startActiveSpan( + "onAction()", + async () => { + return await onAction({ + action: parsedAction as any, + chatId: currentWirePayload.chatId, + turn, + clientData, + uiMessages: accumulatedUIMessages, + messages: accumulatedMessages, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": currentWirePayload.chatId, + "chat.action": + typeof parsedAction === "object" && parsedAction !== null + ? JSON.stringify(parsedAction) + : String(parsedAction), + }, } - } else { - warnMissingOnActionOnce(); + ); + + // Apply chat.history mutations from onAction + const actionOverride = locals.get(chatOverrideMessagesKey); + if (actionOverride) { + locals.set(chatOverrideMessagesKey, undefined); + accumulatedUIMessages = [...actionOverride] as TUIMessage[]; + accumulatedMessages = await toModelMessages(actionOverride); + locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); } + } else { + warnMissingOnActionOnce(); } + } - // ── Message handling (non-action turns) ─────────────────── - // - // Slim wire: at most one delta message arrives per record. - // The accumulator was already seeded at boot from a durable - // snapshot + `session.out` replay (or `hydrateMessages`, - // which also fires per-turn below). Per-turn handling is - // therefore a delta merge, not a full-history reset. - if (currentWirePayload.trigger !== "action") { - + // ── Message handling (non-action turns) ─────────────────── + // + // Slim wire: at most one delta message arrives per record. + // The accumulator was already seeded at boot from a durable + // snapshot + `session.out` replay (or `hydrateMessages`, + // which also fires per-turn below). Per-turn handling is + // therefore a delta merge, not a full-history reset. + if (currentWirePayload.trigger !== "action") { let cleanedUIMessages: TUIMessage[] = cleanedIncomingMessages; // Turn-0 head-start with hydrateMessages: the boot seeding from @@ -6809,8 +6772,7 @@ function chatAgent< ) { const lastUI = cleanedUIMessages[cleanedUIMessages.length - 1]!; const matchedExisting = - lastUI.id !== undefined && - previouslyKnownMessageIds.has(lastUI.id); + lastUI.id !== undefined && previouslyKnownMessageIds.has(lastUI.id); if (!matchedExisting) { turnNewUIMessages.push(lastUI); const lastModel = (await toModelMessages([lastUI]))[0]; @@ -6855,16 +6817,12 @@ function chatAgent< let replaced = false; for (const raw of cleanedUIMessages) { let incoming = raw; - let idx = accumulatedUIMessages.findIndex( - (m) => m.id === incoming.id - ); + let idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id); if (idx === -1) { const rewritten = rewriteIncomingIdViaToolCallMap(incoming); if (rewritten.id !== incoming.id) { incoming = rewritten as typeof raw; - idx = accumulatedUIMessages.findIndex( - (m) => m.id === incoming.id - ); + idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id); } } if (idx !== -1) { @@ -6888,9 +6846,7 @@ function chatAgent< accumulatedMessages.push(...incomingModelMessages); } if (turnNewUIMessages.length > 0) { - turnNewModelMessages.push( - ...(await toModelMessages(turnNewUIMessages)) - ); + turnNewModelMessages.push(...(await toModelMessages(turnNewUIMessages))); } } // `preload` / `close` / `handover-prepare` and submits @@ -6931,57 +6887,54 @@ function chatAgent< } locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); + } // end if (trigger !== "action") + + // ── Action result handling ────────────────────────────── + // For action turns, skip the turn machinery entirely. + // If `onAction` returned a stream / string / UIMessage, + // pipe it as the response. Either way, emit + // `trigger:turn-complete` and then fall through to the + // wait-for-next-message logic (shared with message turns). + // The turn counter is decremented so the next iteration + // sees the same `turn` value β€” actions don't count. + if (isAction) { + msgSub.off(); - } // end if (trigger !== "action") - - // ── Action result handling ────────────────────────────── - // For action turns, skip the turn machinery entirely. - // If `onAction` returned a stream / string / UIMessage, - // pipe it as the response. Either way, emit - // `trigger:turn-complete` and then fall through to the - // wait-for-next-message logic (shared with message turns). - // The turn counter is decremented so the next iteration - // sees the same `turn` value β€” actions don't count. - if (isAction) { - msgSub.off(); - - if ( - (locals.get(chatPipeCountKey) ?? 0) === 0 && - isUIMessageStreamable(actionStreamResult) - ) { - try { - const resolvedOptions = resolveUIMessageStreamOptions(); - const uiStream = ( - actionStreamResult as UIMessageStreamable - ).toUIMessageStream({ - ...resolvedOptions, - generateMessageId: - resolvedOptions.generateMessageId ?? generateMessageId, - }); - await pipeChat(uiStream, { - signal: combinedSignal, - spanName: "stream response", - }); - } catch (error) { - if ( - error instanceof Error && - error.name === "AbortError" && - runSignal.aborted - ) { - return "exit"; - } - throw error; + if ( + (locals.get(chatPipeCountKey) ?? 0) === 0 && + isUIMessageStreamable(actionStreamResult) + ) { + try { + const resolvedOptions = resolveUIMessageStreamOptions(); + const uiStream = ( + actionStreamResult as UIMessageStreamable + ).toUIMessageStream({ + ...resolvedOptions, + generateMessageId: resolvedOptions.generateMessageId ?? generateMessageId, + }); + await pipeChat(uiStream, { + signal: combinedSignal, + spanName: "stream response", + }); + } catch (error) { + if ( + error instanceof Error && + error.name === "AbortError" && + runSignal.aborted + ) { + return "exit"; } + throw error; } - - await writeTurnCompleteChunk(currentWirePayload.chatId); - - // Don't consume a turn iteration β€” actions aren't turns. - turn--; } - if (!isAction) { + await writeTurnCompleteChunk(currentWirePayload.chatId); + // Don't consume a turn iteration β€” actions aren't turns. + turn--; + } + + if (!isAction) { // Mint a scoped public access token once per turn, reused for // onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk. const currentRunId = ctx.run.id; @@ -7159,7 +7112,10 @@ function chatAgent< // Don't call userRun. Don't pipe. Skip directly // to the post-turn flow below. } else { - const preparedMessages = await applyPrepareMessages(accumulatedMessages, "run"); + const preparedMessages = await applyPrepareMessages( + accumulatedMessages, + "run" + ); runResult = await userRun({ ...restWire, messages: preparedMessages, @@ -7182,7 +7138,10 @@ function chatAgent< // We call toUIMessageStream ourselves to attach onFinish for response capture. // Pass originalMessages so the AI SDK reuses message IDs across turns // (e.g. for tool approval continuations / HITL flows). - if ((locals.get(chatPipeCountKey) ?? 0) === 0 && isUIMessageStreamable(runResult)) { + if ( + (locals.get(chatPipeCountKey) ?? 0) === 0 && + isUIMessageStreamable(runResult) + ) { onFinishAttached = true; const resolvedOptions = resolveUIMessageStreamOptions(); // For action turns, don't pass originalMessages: the response @@ -7195,9 +7154,7 @@ function chatAgent< // Pass originalMessages so the AI SDK reuses message IDs across // turns (e.g. for tool approval continuations / HITL flows). // Omit for action turns to force a fresh response ID. - ...(isActionTurn - ? {} - : { originalMessages: accumulatedUIMessages }), + ...(isActionTurn ? {} : { originalMessages: accumulatedUIMessages }), // Always provide generateMessageId so the start chunk carries a // messageId. Without this, the frontend and backend generate IDs // independently and they won't match for ID-based dedup. @@ -7214,7 +7171,10 @@ function chatAgent< resolveOnFinish!(); }, }); - await pipeChat(uiStream, { signal: combinedSignal, spanName: "stream response" }); + await pipeChat(uiStream, { + signal: combinedSignal, + spanName: "stream response", + }); } } catch (error) { // Handle AbortError from streamText gracefully @@ -7248,7 +7208,10 @@ function chatAgent< // never reports final usage), which would block the turn loop // from ever firing onTurnComplete / writeTurnComplete. let turnUsage: LanguageModelUsage | undefined; - if (runResult != null && typeof (runResult as any).totalUsage?.then === "function") { + if ( + runResult != null && + typeof (runResult as any).totalUsage?.then === "function" + ) { try { turnUsage = (await Promise.race([ (runResult as any).totalUsage, @@ -7352,7 +7315,10 @@ function chatAgent< // may produce a message with an empty ID since IDs are normally // assigned by the frontend's useChat). if (!capturedResponseMessage.id) { - capturedResponseMessage = { ...capturedResponseMessage, id: generateMessageId() }; + capturedResponseMessage = { + ...capturedResponseMessage, + id: generateMessageId(), + }; } // Append any non-transient data parts queued via chat.response or writer.write() const queuedParts = locals.get(chatResponsePartsKey); @@ -7369,9 +7335,7 @@ function chatAgent< // instead of pushing a duplicate. For action turns this never // matches because originalMessages is omitted (fresh ID). const existingIdx = capturedResponseMessage.id - ? accumulatedUIMessages.findIndex( - (m) => m.id === capturedResponseMessage!.id - ) + ? accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage!.id) : -1; if (existingIdx !== -1) { accumulatedUIMessages[existingIdx] = capturedResponseMessage; @@ -7494,16 +7458,16 @@ function chatAgent< accumulatedMessages = outerCompaction.compactModelMessages ? await outerCompaction.compactModelMessages(outerCompactEvent) : [ - { - role: "assistant" as const, - content: [ - { - type: "text" as const, - text: `[Conversation summary]\n\n${summary}`, - }, - ], - }, - ]; + { + role: "assistant" as const, + content: [ + { + type: "text" as const, + text: `[Conversation summary]\n\n${summary}`, + }, + ], + }, + ]; // UI messages: callback or default (preserve all) if (outerCompaction.compactUIMessages) { @@ -7531,7 +7495,10 @@ function chatAgent< }); } - compactionSpan.setAttribute("compaction.summary_length", summary.length); + compactionSpan.setAttribute( + "compaction.summary_length", + summary.length + ); write({ type: "data-compaction", @@ -7626,7 +7593,9 @@ function chatAgent< // Drain any late response parts added during onBeforeTurnComplete const lateParts = locals.get(chatResponsePartsKey); if (lateParts && lateParts.length > 0 && capturedResponseMessage) { - const idx = accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage!.id); + const idx = accumulatedUIMessages.findIndex( + (m) => m.id === capturedResponseMessage!.id + ); if (idx !== -1) { const msg = accumulatedUIMessages[idx]!; accumulatedUIMessages[idx] = { @@ -7688,7 +7657,9 @@ function chatAgent< ? { "gen_ai.usage.total_tokens": turnUsage.totalTokens } : {}), ...(cumulativeUsage.totalTokens != null - ? { "gen_ai.usage.cumulative_total_tokens": cumulativeUsage.totalTokens } + ? { + "gen_ai.usage.cumulative_total_tokens": cumulativeUsage.totalTokens, + } : {}), }, } @@ -7718,17 +7689,14 @@ function chatAgent< await tracer.startActiveSpan( "snapshot.write", async () => { - const snapshotInCursor = - getChatSession().in.lastDispatchedSeqNum(); + const snapshotInCursor = getChatSession().in.lastDispatchedSeqNum(); await writeChatSnapshot(sessionIdForSnapshot, { version: 1, savedAt: Date.now(), messages: accumulatedUIMessages, lastOutEventId: turnCompleteResult?.lastEventId, lastInEventId: - snapshotInCursor !== undefined - ? String(snapshotInCursor) - : undefined, + snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined, }); }, { @@ -7751,54 +7719,50 @@ function chatAgent< ); } } + } // end if (!isAction) + + // NOTE: We intentionally do NOT await deferred work from onTurnComplete here. + // Promises deferred in onTurnComplete (e.g. background self-review via + // chat.defer + chat.inject) run during the idle wait. If they complete + // before the next message, their injected context is picked up in prepareStep. + // The pre-onBeforeTurnComplete drain handles promises from onTurnStart/run(). + + // Recovery-boot injection: drain remaining recovered turns + // before any other source. `onRecoveryBoot` (or its default) + // produced these from in-flight user messages on session.in + // that the dead predecessor never acknowledged. + if (bootInjectedQueue.length > 0) { + currentWirePayload = bootInjectedQueue.shift()!; + return "continue"; + } - } // end if (!isAction) - - // NOTE: We intentionally do NOT await deferred work from onTurnComplete here. - // Promises deferred in onTurnComplete (e.g. background self-review via - // chat.defer + chat.inject) run during the idle wait. If they complete - // before the next message, their injected context is picked up in prepareStep. - // The pre-onBeforeTurnComplete drain handles promises from onTurnStart/run(). - - // Recovery-boot injection: drain remaining recovered turns - // before any other source. `onRecoveryBoot` (or its default) - // produced these from in-flight user messages on session.in - // that the dead predecessor never acknowledged. - if (bootInjectedQueue.length > 0) { - currentWirePayload = bootInjectedQueue.shift()!; - return "continue"; - } - - // If messages arrived during streaming (without pendingMessages config), - // use the first one immediately as the next turn. - if (pendingMessages.length > 0) { - currentWirePayload = pendingMessages[0]!; - return "continue"; - } + // If messages arrived during streaming (without pendingMessages config), + // use the first one immediately as the next turn. + if (pendingMessages.length > 0) { + currentWirePayload = pendingMessages[0]!; + return "continue"; + } - // chat.requestUpgrade() was called β€” exit the loop so the - // transport triggers a new run on the latest version. - // chat.endRun() β€” same exit, no upgrade semantics. - if ( - locals.get(chatUpgradeRequestedKey) || - locals.get(chatEndRunRequestedKey) - ) { - return "exit"; - } + // chat.requestUpgrade() was called β€” exit the loop so the + // transport triggers a new run on the latest version. + // chat.endRun() β€” same exit, no upgrade semantics. + if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) { + return "exit"; + } - // Wait for the next message β€” stay idle briefly, then suspend - const effectiveIdleTimeout = - (metadata.get(IDLE_TIMEOUT_METADATA_KEY) as number | undefined) ?? - idleTimeoutInSeconds; - const effectiveTurnTimeout = - (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; - - const next = await messagesInput.waitWithIdleTimeout({ - idleTimeoutInSeconds: effectiveIdleTimeout, - timeout: effectiveTurnTimeout, - spanName: "waiting for next message", - onSuspend: onChatSuspend - ? async () => { + // Wait for the next message β€” stay idle briefly, then suspend + const effectiveIdleTimeout = + (metadata.get(IDLE_TIMEOUT_METADATA_KEY) as number | undefined) ?? + idleTimeoutInSeconds; + const effectiveTurnTimeout = + (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; + + const next = await messagesInput.waitWithIdleTimeout({ + idleTimeoutInSeconds: effectiveIdleTimeout, + timeout: effectiveTurnTimeout, + spanName: "waiting for next message", + onSuspend: onChatSuspend + ? async () => { await tracer.startActiveSpan( "onChatSuspend()", async () => { @@ -7824,9 +7788,9 @@ function chatAgent< } ); } - : undefined, - onResume: onChatResume - ? async () => { + : undefined, + onResume: onChatResume + ? async () => { await tracer.startActiveSpan( "onChatResume()", async () => { @@ -7852,204 +7816,202 @@ function chatAgent< } ); } - : undefined, - }); - - if (!next.ok) { - return "exit"; - } + : undefined, + }); - currentWirePayload = next.output as ChatTaskWirePayload< - TUIMessage, - inferSchemaIn - >; + if (!next.ok) { + return "exit"; + } - // Close signal β€” exit the loop gracefully - if (currentWirePayload.trigger === "close") { - return "exit"; - } + currentWirePayload = next.output as ChatTaskWirePayload< + TUIMessage, + inferSchemaIn + >; - return "continue"; - }, - { - attributes: turnAttributes, + // Close signal β€” exit the loop gracefully + if (currentWirePayload.trigger === "close") { + return "exit"; } - ); - if (turnResult === "exit") return; - // "continue" means proceed to next iteration - } catch (turnError) { - // Turn error handler: write an error chunk + turn-complete to the stream - // so the client sees the error, then wait for the next message instead - // of killing the entire run. This keeps the conversation alive. - if (turnError instanceof Error && turnError.name === "AbortError" && runSignal.aborted) { - // Full run cancellation β€” exit immediately - throw turnError; + return "continue"; + }, + { + attributes: turnAttributes, } + ); - // OOM errors must escape the turn loop so the task runtime can - // honor `retry.outOfMemory.machine` (set on chat.agent via - // `oomMachine`). Catching them here would keep the dead worker - // alive and defeat the machine swap. Re-throw and let the - // runtime dispatch the retry on a larger machine; recovery on - // attempt 2 picks up via the standard continuation path - // (same chatId / Session, accumulator rehydrates). - if (turnError instanceof OutOfMemoryError) { - throw turnError; - } + if (turnResult === "exit") return; + // "continue" means proceed to next iteration + } catch (turnError) { + // Turn error handler: write an error chunk + turn-complete to the stream + // so the client sees the error, then wait for the next message instead + // of killing the entire run. This keeps the conversation alive. + if ( + turnError instanceof Error && + turnError.name === "AbortError" && + runSignal.aborted + ) { + // Full run cancellation β€” exit immediately + throw turnError; + } - let errorTurnCompleteResult: Awaited< - ReturnType - > | undefined; - try { - await withChatWriter(async (writer) => { - const errorText = - turnError instanceof Error ? turnError.message : "An unexpected error occurred"; - writer.write({ type: "error", errorText } as any); - }); - // Signal turn complete so the client knows this turn is done - errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId); - } catch { - // Best-effort β€” if stream write fails, let the run continue anyway - } + // OOM errors must escape the turn loop so the task runtime can + // honor `retry.outOfMemory.machine` (set on chat.agent via + // `oomMachine`). Catching them here would keep the dead worker + // alive and defeat the machine swap. Re-throw and let the + // runtime dispatch the retry on a larger machine; recovery on + // attempt 2 picks up via the standard continuation path + // (same chatId / Session, accumulator rehydrates). + if (turnError instanceof OutOfMemoryError) { + throw turnError; + } - // The submit-message merge into the accumulator may not have run - // yet (a pre-run hook threw), so fold the wire message in for the - // error event + snapshot β€” the cursor has already advanced past it, - // so otherwise it survives in neither the snapshot nor the `.in` tail. - const erroredWireMessage = (currentWirePayload as { message?: TUIMessage }).message; - const erroredUIMessages = - erroredWireMessage && - !accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id) - ? [...accumulatedUIMessages, erroredWireMessage] - : accumulatedUIMessages; - - // Fire onTurnComplete on the error path too β€” the docs promise it - // runs "after every turn, successful or errored" so customers can - // mark the turn failed. `responseMessage` is undefined/partial and - // `error` carries the thrown value. - if (onTurnComplete) { - try { - await tracer.startActiveSpan( - "onTurnComplete()", - async () => { - await onTurnComplete({ - ctx, - chatId: currentWirePayload.chatId, - messages: accumulatedMessages, - uiMessages: erroredUIMessages, - newMessages: [], - newUIMessages: erroredWireMessage ? [erroredWireMessage] : [], - responseMessage: undefined, - rawResponseMessage: undefined, - turn, - runId: ctx.run.id, - chatAccessToken: "", - // Parsed `clientData` isn't reliably in scope here (parsing - // may itself be the failure), and the raw metadata is the - // wrong shape β€” leave it undefined on the error path. - clientData: undefined, - stopped: false, - continuation, - previousRunId, - preloaded, - totalUsage: cumulativeUsage, - finishReason: "error", - error: turnError, - lastEventId: errorTurnCompleteResult?.lastEventId, - }); + let errorTurnCompleteResult: + | Awaited> + | undefined; + try { + await withChatWriter(async (writer) => { + const errorText = + turnError instanceof Error ? turnError.message : "An unexpected error occurred"; + writer.write({ type: "error", errorText } as any); + }); + // Signal turn complete so the client knows this turn is done + errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId); + } catch { + // Best-effort β€” if stream write fails, let the run continue anyway + } + + // The submit-message merge into the accumulator may not have run + // yet (a pre-run hook threw), so fold the wire message in for the + // error event + snapshot β€” the cursor has already advanced past it, + // so otherwise it survives in neither the snapshot nor the `.in` tail. + const erroredWireMessage = (currentWirePayload as { message?: TUIMessage }).message; + const erroredUIMessages = + erroredWireMessage && + !accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id) + ? [...accumulatedUIMessages, erroredWireMessage] + : accumulatedUIMessages; + + // Fire onTurnComplete on the error path too β€” the docs promise it + // runs "after every turn, successful or errored" so customers can + // mark the turn failed. `responseMessage` is undefined/partial and + // `error` carries the thrown value. + if (onTurnComplete) { + try { + await tracer.startActiveSpan( + "onTurnComplete()", + async () => { + await onTurnComplete({ + ctx, + chatId: currentWirePayload.chatId, + messages: accumulatedMessages, + uiMessages: erroredUIMessages, + newMessages: [], + newUIMessages: erroredWireMessage ? [erroredWireMessage] : [], + responseMessage: undefined, + rawResponseMessage: undefined, + turn, + runId: ctx.run.id, + chatAccessToken: "", + // Parsed `clientData` isn't reliably in scope here (parsing + // may itself be the failure), and the raw metadata is the + // wrong shape β€” leave it undefined on the error path. + clientData: undefined, + stopped: false, + continuation, + previousRunId, + preloaded, + totalUsage: cumulativeUsage, + finishReason: "error", + error: turnError, + lastEventId: errorTurnCompleteResult?.lastEventId, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": currentWirePayload.chatId, + "chat.turn": turn + 1, + "chat.errored": true, }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": currentWirePayload.chatId, - "chat.turn": turn + 1, - "chat.errored": true, - }, - } - ); - } catch { - // A throwing onTurnComplete on the error path must not crash - // the run β€” keep the conversation alive for the next message. - } + } + ); + } catch { + // A throwing onTurnComplete on the error path must not crash + // the run β€” keep the conversation alive for the next message. } + } - // Persist a snapshot so the failed turn's user message isn't - // stranded. `writeTurnCompleteChunk` already advanced the `.in` - // cursor past it (via the session-in-event-id header), and the - // success-path snapshot write is skipped on error β€” without this - // the next boot would resume past a message that exists in - // neither the snapshot nor the replayable `.in` tail. - if (!hydrateMessages) { - try { - const errorSnapshotInCursor = - getChatSession().in.lastDispatchedSeqNum(); - await writeChatSnapshot(sessionIdForSnapshot, { - version: 1, - savedAt: Date.now(), - messages: erroredUIMessages, - lastOutEventId: errorTurnCompleteResult?.lastEventId, - lastInEventId: - errorSnapshotInCursor !== undefined - ? String(errorSnapshotInCursor) - : undefined, - }); - } catch (error) { - logger.warn("chat.agent: error-path snapshot write failed", { - error: error instanceof Error ? error.message : String(error), - sessionId: sessionIdForSnapshot, - }); - } + // Persist a snapshot so the failed turn's user message isn't + // stranded. `writeTurnCompleteChunk` already advanced the `.in` + // cursor past it (via the session-in-event-id header), and the + // success-path snapshot write is skipped on error β€” without this + // the next boot would resume past a message that exists in + // neither the snapshot nor the replayable `.in` tail. + if (!hydrateMessages) { + try { + const errorSnapshotInCursor = getChatSession().in.lastDispatchedSeqNum(); + await writeChatSnapshot(sessionIdForSnapshot, { + version: 1, + savedAt: Date.now(), + messages: erroredUIMessages, + lastOutEventId: errorTurnCompleteResult?.lastEventId, + lastInEventId: + errorSnapshotInCursor !== undefined ? String(errorSnapshotInCursor) : undefined, + }); + } catch (error) { + logger.warn("chat.agent: error-path snapshot write failed", { + error: error instanceof Error ? error.message : String(error), + sessionId: sessionIdForSnapshot, + }); } + } - // chat.requestUpgrade() / chat.endRun() β€” exit after error turn too - if ( - locals.get(chatUpgradeRequestedKey) || - locals.get(chatEndRunRequestedKey) - ) { - return; - } + // chat.requestUpgrade() / chat.endRun() β€” exit after error turn too + if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) { + return; + } - // Drain remaining recovered turns before idling β€” a thrown - // recovered turn shouldn't strand the rest of the boot queue - // until an unrelated live message arrives. - if (bootInjectedQueue.length > 0) { - currentWirePayload = bootInjectedQueue.shift()!; - continue; - } + // Drain remaining recovered turns before idling β€” a thrown + // recovered turn shouldn't strand the rest of the boot queue + // until an unrelated live message arrives. + if (bootInjectedQueue.length > 0) { + currentWirePayload = bootInjectedQueue.shift()!; + continue; + } - // Wait for the next message β€” same as after a successful turn - const effectiveIdleTimeout = - (metadata.get(IDLE_TIMEOUT_METADATA_KEY) as number | undefined) ?? - idleTimeoutInSeconds; - const effectiveTurnTimeout = - (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; - - const next = await messagesInput.waitWithIdleTimeout({ - idleTimeoutInSeconds: effectiveIdleTimeout, - timeout: effectiveTurnTimeout, - spanName: "waiting for next message (after error)", - }); + // Wait for the next message β€” same as after a successful turn + const effectiveIdleTimeout = + (metadata.get(IDLE_TIMEOUT_METADATA_KEY) as number | undefined) ?? + idleTimeoutInSeconds; + const effectiveTurnTimeout = + (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; - if (!next.ok) { - return; // Timed out β€” end run gracefully - } + const next = await messagesInput.waitWithIdleTimeout({ + idleTimeoutInSeconds: effectiveIdleTimeout, + timeout: effectiveTurnTimeout, + spanName: "waiting for next message (after error)", + }); - currentWirePayload = next.output as ChatTaskWirePayload< - TUIMessage, - inferSchemaIn - >; - // Continue to next iteration of the for loop + if (!next.ok) { + return; // Timed out β€” end run gracefully } + + currentWirePayload = next.output as ChatTaskWirePayload< + TUIMessage, + inferSchemaIn + >; + // Continue to next iteration of the for loop } - } finally { - // `stopSub` is registered post-preload so the close-during-preload - // early-return path may exit before it ever attached. Guard the - // cleanup so a missing subscription doesn't throw. - stopSub?.off(); } - } + } finally { + // `stopSub` is registered post-preload so the close-during-preload + // early-return path may exit before it ever attached. Guard the + // cleanup so a missing subscription doesn't throw. + stopSub?.off(); + } + }, }); // Register clientDataSchema so the CLI converts it to JSONSchema @@ -8145,7 +8107,9 @@ export interface ChatBuilder< ): ChatBuilder; /** Register a builder-level `onCompacted` hook. Runs before the task-level hook if both are set. */ - onCompacted(fn: (event: CompactedEvent) => Promise | void): ChatBuilder; + onCompacted( + fn: (event: CompactedEvent) => Promise | void + ): ChatBuilder; /** Register a builder-level `onChatSuspend` hook. Runs before the task-level hook if both are set. */ onChatSuspend( @@ -8169,12 +8133,24 @@ export interface ChatBuilder< * (backwards compatible). */ agent: [TClientDataSchema] extends [undefined] - ? ( - options: ChatAgentOptions - ) => Task>, unknown> - : ( - options: Omit, "clientDataSchema"> - ) => Task>, unknown>; + ? < + TId extends string, + TInfer extends TaskSchema | undefined = undefined, + TAction extends TaskSchema | undefined = undefined, + TTools extends ToolSet = ToolSet, + >( + options: ChatAgentOptions + ) => Task>, unknown> + : < + TId extends string, + TAction extends TaskSchema | undefined = undefined, + TTools extends ToolSet = ToolSet, + >( + options: Omit< + ChatAgentOptions, + "clientDataSchema" + > + ) => Task>, unknown>; /** * Create a custom agent with manual lifecycle control. @@ -8246,9 +8222,7 @@ function createChatBuilder< }); }, - onBoot( - fn: (event: BootEvent>) => Promise | void - ) { + onBoot(fn: (event: BootEvent>) => Promise | void) { return createChatBuilder({ ...config, hooks: { ...config.hooks, onBoot: fn }, @@ -8331,7 +8305,7 @@ function createChatBuilder< const mergedUiStream = config.uiStreamOptions && options.uiMessageStreamOptions ? { ...config.uiStreamOptions, ...options.uiMessageStreamOptions } - : options.uiMessageStreamOptions ?? config.uiStreamOptions; + : (options.uiMessageStreamOptions ?? config.uiStreamOptions); return chatAgent({ ...options, @@ -9075,9 +9049,9 @@ class ChatMessageAccumulator { */ prepareStep(): | ((args: { - messages: ModelMessage[]; - steps: CompactionStep[]; - }) => Promise<{ messages: ModelMessage[] } | undefined>) + messages: ModelMessage[]; + steps: CompactionStep[]; + }) => Promise<{ messages: ModelMessage[] } | undefined>) | undefined { if (!this._compaction && !this._pendingMessages) return undefined; const comp = this._compaction; @@ -9166,11 +9140,11 @@ class ChatMessageAccumulator { this.modelMessages = this._compaction.compactModelMessages ? await this._compaction.compactModelMessages(compactEvent) : [ - { - role: "assistant" as const, - content: [{ type: "text" as const, text: `[Conversation summary]\n\n${summary}` }], - }, - ]; + { + role: "assistant" as const, + content: [{ type: "text" as const, text: `[Conversation summary]\n\n${summary}` }], + }, + ]; if (this._compaction.compactUIMessages) { this.uiMessages = await this._compaction.compactUIMessages(compactEvent); @@ -9265,9 +9239,9 @@ export type ChatTurn = { */ prepareStep(): | ((args: { - messages: ModelMessage[]; - steps: CompactionStep[]; - }) => Promise<{ messages: ModelMessage[] } | undefined>) + messages: ModelMessage[]; + steps: CompactionStep[]; + }) => Promise<{ messages: ModelMessage[] } | undefined>) | undefined; }; @@ -9355,7 +9329,9 @@ function createChatSession( const signal = await waitForHandover({ payload: currentPayload, idleTimeoutInSeconds: - sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? idleTimeoutInSeconds, + sessionIdleTimeoutOpt ?? + currentPayload.idleTimeoutInSeconds ?? + idleTimeoutInSeconds, timeout, }); if (!signal || signal.kind === "handover-skip" || runSignal.aborted) { @@ -9376,7 +9352,10 @@ function createChatSession( // a turn immediately would invoke the model with no user input. const isMessagelessContinuationBoot = currentPayload.continuation === true && !currentPayload.message; - if (turn === 0 && (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) { + if ( + turn === 0 && + (currentPayload.trigger === "preload" || isMessagelessContinuationBoot) + ) { const result = await messagesInput.waitWithIdleTimeout({ idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30, @@ -9403,10 +9382,7 @@ function createChatSession( // Subsequent turns: wait for the next message if (turn > 0) { // chat.requestUpgrade() / chat.endRun() β€” exit before waiting - if ( - locals.get(chatUpgradeRequestedKey) || - locals.get(chatEndRunRequestedKey) - ) { + if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) { stop.cleanup(); return { done: true, value: undefined }; } @@ -9549,9 +9525,7 @@ function createChatSession( } const response = accumulator.uiMessages.at(-1); if (!response || response.role !== "assistant") { - throw new Error( - "turn.complete() could not find the spliced handover response" - ); + throw new Error("turn.complete() could not find the spliced handover response"); } sessionMsgSub.off(); await chatWriteTurnComplete(); @@ -9667,18 +9641,17 @@ function createChatSession( accumulator.modelMessages = sessionCompaction.compactModelMessages ? await sessionCompaction.compactModelMessages(compactEvent) : [ - { - role: "assistant" as const, - content: [ - { type: "text" as const, text: `[Conversation summary]\n\n${summary}` }, - ], - }, - ]; + { + role: "assistant" as const, + content: [ + { type: "text" as const, text: `[Conversation summary]\n\n${summary}` }, + ], + }, + ]; if (sessionCompaction.compactUIMessages) { - accumulator.uiMessages = await sessionCompaction.compactUIMessages( - compactEvent - ); + accumulator.uiMessages = + await sessionCompaction.compactUIMessages(compactEvent); } } } @@ -9692,7 +9665,10 @@ function createChatSession( // Append any non-transient data parts queued via chat.response or writer.write() const queuedParts = locals.get(chatResponsePartsKey); if (queuedParts && queuedParts.length > 0) { - response = { ...response, parts: [...(response.parts ?? []), ...(queuedParts as UIMessage["parts"])] }; + response = { + ...response, + parts: [...(response.parts ?? []), ...(queuedParts as UIMessage["parts"])], + }; locals.set(chatResponsePartsKey, []); } await accumulator.addResponse(response); @@ -9929,8 +9905,8 @@ function chatLocal>(options: { id: string }): if (current === undefined) { throw new Error( "chat.local can only be modified after initialization. " + - "Call local.init() in onBoot (recommended β€” fires on every fresh worker including continuation runs) or run() first. " + - "If you previously initialized in onChatStart, move it to onBoot β€” onChatStart only fires on the chat's very first message and will not run on a continuation." + "Call local.init() in onBoot (recommended β€” fires on every fresh worker including continuation runs) or run() first. " + + "If you previously initialized in onChatStart, move it to onBoot β€” onChatStart only fires on the chat's very first message and will not run on a continuation." ); } locals.set(localKey, { ...current, [prop]: value }); @@ -10018,9 +9994,7 @@ export type ChatStartSessionEndpointContext = { chatId: string; }; -export type ChatStartSessionBaseURLResolver = ( - ctx: ChatStartSessionEndpointContext -) => string; +export type ChatStartSessionBaseURLResolver = (ctx: ChatStartSessionEndpointContext) => string; export type ChatStartSessionFetchOverride = ( url: string, @@ -10196,15 +10170,13 @@ function createChatStartSessionAction( ...(options?.triggerConfig?.maxAttempts !== undefined || params.triggerConfig?.maxAttempts !== undefined ? { - maxAttempts: - params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts!, + maxAttempts: params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts!, } : {}), ...(options?.triggerConfig?.maxDuration !== undefined || params.triggerConfig?.maxDuration !== undefined ? { - maxDuration: - params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration!, + maxDuration: params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration!, } : {}), ...(options?.triggerConfig?.region || params.triggerConfig?.region @@ -10283,10 +10255,7 @@ function resolveChatStartBaseURL( option: string | ChatStartSessionBaseURLResolver | undefined ): string { const fallback = apiClientManager.baseURL ?? "https://api.trigger.dev"; - const raw = - typeof option === "function" - ? option({ endpoint, chatId }) - : option ?? fallback; + const raw = typeof option === "function" ? option({ endpoint, chatId }) : (option ?? fallback); return raw.replace(/\/$/, ""); } @@ -10308,7 +10277,13 @@ function overrideRequestHeaders(accessToken: string): Record { async function callSessionsCreateWithOverride(args: { chatId: string; - body: { type: "chat.agent"; externalId: string; taskIdentifier: string; triggerConfig: SessionTriggerConfig; metadata?: Record }; + body: { + type: "chat.agent"; + externalId: string; + taskIdentifier: string; + triggerConfig: SessionTriggerConfig; + metadata?: Record; + }; baseURLOption: string | ChatStartSessionBaseURLResolver | undefined; fetchOverride: ChatStartSessionFetchOverride | undefined; }): Promise<{ id: string; runId: string; publicAccessToken: string }> { @@ -10344,9 +10319,7 @@ async function mintPublicTokenWithOverride(args: { }): Promise { const accessToken = apiClientManager.accessToken; if (!accessToken) { - throw new Error( - "chat.createStartSessionAction: no API access token configured for JWT mint." - ); + throw new Error("chat.createStartSessionAction: no API access token configured for JWT mint."); } const ctx: ChatStartSessionEndpointContext = { endpoint: "auth", chatId: args.chatId }; const url = `${resolveChatStartBaseURL("auth", args.chatId, args.baseURLOption)}/api/v1/auth/jwt/claims`; diff --git a/packages/trigger-sdk/src/v3/chat-client.ts b/packages/trigger-sdk/src/v3/chat-client.ts index 28d015a96f..9836346fad 100644 --- a/packages/trigger-sdk/src/v3/chat-client.ts +++ b/packages/trigger-sdk/src/v3/chat-client.ts @@ -43,9 +43,7 @@ export type InferChatClientData = /** Extract the UIMessage type from a chat agent task. */ export type InferChatUIMessage = - T extends Task, any> - ? TUIMessage - : UIMessage; + T extends Task, any> ? TUIMessage : UIMessage; // ─── Types ───────────────────────────────────────────────────────── @@ -100,10 +98,7 @@ export type AgentChatOptions = { * Called when a turn completes. Persist `lastEventId` for stream * resumption across requests. */ - onTurnComplete?: (event: { - chatId: string; - lastEventId?: string; - }) => void | Promise; + onTurnComplete?: (event: { chatId: string; lastEventId?: string }) => void | Promise; /** SSE timeout in seconds. @default 120 */ streamTimeoutSeconds?: number; /** @@ -328,9 +323,10 @@ export class AgentChat { this.onTriggered = options.onTriggered; this.onTurnComplete = options.onTurnComplete; const baseURLOption = options.baseURL; - this.baseURLResolver = typeof baseURLOption === "function" - ? baseURLOption - : () => baseURLOption ?? apiClientManager.baseURL ?? "https://api.trigger.dev"; + this.baseURLResolver = + typeof baseURLOption === "function" + ? baseURLOption + : () => baseURLOption ?? apiClientManager.baseURL ?? "https://api.trigger.dev"; this.fetchOverride = options.fetch; // Hydration: a non-empty `session` means the caller knows the @@ -372,10 +368,7 @@ export class AgentChat { * const text = await stream.text(); * ``` */ - async sendMessage( - text: string, - options?: { abortSignal?: AbortSignal } - ): Promise { + async sendMessage(text: string, options?: { abortSignal?: AbortSignal }): Promise { const msgId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const message: UIMessage = { id: msgId, @@ -389,12 +382,14 @@ export class AgentChat { /** Send raw UIMessage-like objects. Use `sendMessage()` for simple text. */ async sendRaw( - messages: UIMessage[] | Array<{ - id: string; - role: string; - parts?: unknown[]; - [key: string]: unknown; - }>, + messages: + | UIMessage[] + | Array<{ + id: string; + role: string; + parts?: unknown[]; + [key: string]: unknown; + }>, options?: { trigger?: "submit-message" | "regenerate-message"; abortSignal?: AbortSignal; @@ -416,9 +411,7 @@ export class AgentChat { // payload β€” reasoning blobs, text, and tool `input` come from the // agent's authoritative chain. `regenerate-message` omits `message`. if (triggerType === "submit-message" && messages.length === 0) { - throw new Error( - "AgentChat.sendRaw: 'submit-message' trigger requires at least one message" - ); + throw new Error("AgentChat.sendRaw: 'submit-message' trigger requires at least one message"); } const lastIfSubmit = triggerType === "submit-message" @@ -542,10 +535,7 @@ export class AgentChat { * } * ``` */ - async sendAction( - action: unknown, - options?: { abortSignal?: AbortSignal } - ): Promise { + async sendAction(action: unknown, options?: { abortSignal?: AbortSignal }): Promise { await this.ensureStarted(); const payload: ChatTaskWirePayload = { @@ -587,9 +577,7 @@ export class AgentChat { } /** Reconnect to the response stream (e.g. after a disconnect). */ - async reconnect( - abortSignal?: AbortSignal - ): Promise | null> { + async reconnect(abortSignal?: AbortSignal): Promise | null> { if (!this.state.started) return null; return this.subscribeToSessionStream(abortSignal, { sendStopOnAbort: false }); } @@ -662,15 +650,9 @@ export class AgentChat { chatId: this.chatId, ...(this.clientData ? { metadata: this.clientData } : {}), }, - ...(this.triggerConfigDefault?.machine - ? { machine: this.triggerConfigDefault.machine } - : {}), - ...(this.triggerConfigDefault?.queue - ? { queue: this.triggerConfigDefault.queue } - : {}), - ...(this.triggerConfigDefault?.tags - ? { tags: this.triggerConfigDefault.tags } - : {}), + ...(this.triggerConfigDefault?.machine ? { machine: this.triggerConfigDefault.machine } : {}), + ...(this.triggerConfigDefault?.queue ? { queue: this.triggerConfigDefault.queue } : {}), + ...(this.triggerConfigDefault?.tags ? { tags: this.triggerConfigDefault.tags } : {}), ...(this.triggerConfigDefault?.maxAttempts !== undefined ? { maxAttempts: this.triggerConfigDefault.maxAttempts } : {}), @@ -678,8 +660,7 @@ export class AgentChat { this.triggerConfigDefault?.idleTimeoutInSeconds !== undefined ? { idleTimeoutInSeconds: - options?.idleTimeoutInSeconds ?? - this.triggerConfigDefault?.idleTimeoutInSeconds!, + options?.idleTimeoutInSeconds ?? this.triggerConfigDefault?.idleTimeoutInSeconds!, } : {}), }; @@ -709,7 +690,7 @@ export class AgentChat { const sseCtx: AgentChatEndpointContext = { endpoint: "out", chatId }; const fetchOverride = this.fetchOverride; const sseFetchClient: typeof fetch | undefined = fetchOverride - ? ((input, init) => { + ? (((input, init) => { if (typeof input === "string") { return fetchOverride(input, init ?? {}, sseCtx); } @@ -728,7 +709,7 @@ export class AgentChat { }, sseCtx ); - }) as typeof fetch + }) as typeof fetch) : undefined; const internalAbort = new AbortController(); diff --git a/packages/trigger-sdk/src/v3/chat-react.ts b/packages/trigger-sdk/src/v3/chat-react.ts index 495e235083..5995667730 100644 --- a/packages/trigger-sdk/src/v3/chat-react.ts +++ b/packages/trigger-sdk/src/v3/chat-react.ts @@ -174,10 +174,7 @@ export function useMultiTabChat( useEffect(() => { if (!transport.hasClaim(chatId) && latestMessagesRef.current.length > 0) { if (idleRef.current !== null) { - const cancel = - typeof cancelIdleCallback === "function" - ? cancelIdleCallback - : clearTimeout; + const cancel = typeof cancelIdleCallback === "function" ? cancelIdleCallback : clearTimeout; cancel(idleRef.current as any); idleRef.current = null; } diff --git a/packages/trigger-sdk/src/v3/chat-server.test.ts b/packages/trigger-sdk/src/v3/chat-server.test.ts index e6caaa40b0..539fe0247a 100644 --- a/packages/trigger-sdk/src/v3/chat-server.test.ts +++ b/packages/trigger-sdk/src/v3/chat-server.test.ts @@ -201,8 +201,8 @@ describe("chat.headStart (route handler)", () => { expect(res.headers.get("X-Trigger-Chat-Access-Token")).toBe(SESSION_PAT); expect(res.headers.get("Content-Type")).toMatch(/text\/event-stream/); - const sessionCreate = requests.find((r) => - r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/") + const sessionCreate = requests.find( + (r) => r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/") ); expect(sessionCreate).toBeDefined(); const body = JSON.parse(sessionCreate!.init!.body as string); @@ -228,10 +228,17 @@ describe("chat.headStart (route handler)", () => { return appendOkResponse(); } if (/\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr)) { - return new Response(new ReadableStream({ start(c) { c.close(); } }), { - status: 200, - headers: { "content-type": "text/event-stream" }, - }); + return new Response( + new ReadableStream({ + start(c) { + c.close(); + }, + }), + { + status: 200, + headers: { "content-type": "text/event-stream" }, + } + ); } throw new Error(`Unexpected URL: ${urlStr}`); }); @@ -262,16 +269,12 @@ describe("chat.headStart (route handler)", () => { ) ); - const sessionCreate = requests.find((r) => - r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/") + const sessionCreate = requests.find( + (r) => r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/") ); expect(sessionCreate).toBeDefined(); const body = JSON.parse(sessionCreate!.init!.body as string); - expect(body.triggerConfig.tags).toEqual([ - "chat:chat-1", - "org:acme", - "agentic-run:xyz", - ]); + expect(body.triggerConfig.tags).toEqual(["chat:chat-1", "org:acme", "agentic-run:xyz"]); expect(body.triggerConfig.queue).toBe("my-queue"); expect(body.triggerConfig.basePayload.trigger).toBe("handover-prepare"); expect(body.triggerConfig.basePayload.chatId).toBe("chat-1"); @@ -290,10 +293,17 @@ describe("chat.headStart (route handler)", () => { } // Stitched response subscribes to `.out` after handover. if (/\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr)) { - return new Response(new ReadableStream({ start(c) { c.close(); } }), { - status: 200, - headers: { "content-type": "text/event-stream" }, - }); + return new Response( + new ReadableStream({ + start(c) { + c.close(); + }, + }), + { + status: 200, + headers: { "content-type": "text/event-stream" }, + } + ); } throw new Error(`Unexpected URL: ${urlStr}`); }); @@ -332,8 +342,7 @@ describe("chat.headStart (route handler)", () => { const handoverPost = requests.find( (r) => - r.url.includes("/realtime/v1/sessions/chat-final/in/append") && - r.init?.body !== undefined + r.url.includes("/realtime/v1/sessions/chat-final/in/append") && r.init?.body !== undefined ); expect(handoverPost).toBeDefined(); const body = JSON.parse(handoverPost!.init!.body as string); @@ -366,10 +375,17 @@ describe("chat.headStart (route handler)", () => { // closes immediately β€” this test validates dispatch only, not // the agent-side resume. if (/\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr)) { - return new Response(new ReadableStream({ start(c) { c.close(); } }), { - status: 200, - headers: { "content-type": "text/event-stream" }, - }); + return new Response( + new ReadableStream({ + start(c) { + c.close(); + }, + }), + { + status: 200, + headers: { "content-type": "text/event-stream" }, + } + ); } throw new Error(`Unexpected URL: ${urlStr}`); }); @@ -412,8 +428,7 @@ describe("chat.headStart (route handler)", () => { const handoverPost = requests.find( (r) => - r.url.includes("/realtime/v1/sessions/chat-tool/in/append") && - r.init?.body !== undefined + r.url.includes("/realtime/v1/sessions/chat-tool/in/append") && r.init?.body !== undefined ); expect(handoverPost).toBeDefined(); const body = JSON.parse(handoverPost!.init!.body as string); @@ -430,9 +445,7 @@ describe("chat.headStart (route handler)", () => { (m: { role: string }) => m.role === "assistant" ); expect(assistant).toBeDefined(); - const toolCallPart = assistant.content.find( - (p: { type: string }) => p.type === "tool-call" - ); + const toolCallPart = assistant.content.find((p: { type: string }) => p.type === "tool-call"); expect(toolCallPart).toBeDefined(); const approvalRequestPart = assistant.content.find( (p: { type: string }) => p.type === "tool-approval-request" diff --git a/packages/trigger-sdk/src/v3/chat-server.ts b/packages/trigger-sdk/src/v3/chat-server.ts index a20bc7ac22..f058593393 100644 --- a/packages/trigger-sdk/src/v3/chat-server.ts +++ b/packages/trigger-sdk/src/v3/chat-server.ts @@ -145,9 +145,7 @@ export type HeadStartSession = { * fire-and-forget. Returns a passthrough that the caller can use as * the HTTP response body. */ - tee( - stream: ReadableStream - ): ReadableStream; + tee(stream: ReadableStream): ReadableStream; /** * Awaits `result.finishReason` and dispatches `handover` (with the * partial assistant ModelMessages) or `handover-skip`. @@ -593,9 +591,9 @@ async function openHandoverSession(opts: { idleTimeoutInSeconds * 1000 ); - const buildStreamTextOptions = ( - spreadOpts?: { tools?: Record } - ): Record => { + const buildStreamTextOptions = (spreadOpts?: { + tools?: Record; + }): Record => { // The customer spreads this object into their `streamText` call // and then adds `model`, `system`, etc. on top. We set the four // keys handover correctness depends on: @@ -850,9 +848,7 @@ async function openHandoverSession(opts: { chatId, "out", { - ...(customerLastEventId != null - ? { lastEventId: customerLastEventId } - : {}), + ...(customerLastEventId != null ? { lastEventId: customerLastEventId } : {}), signal: AbortSignal.any([abortController.signal, subscriptionAbort.signal]), onPart: (part) => { if (part.id) latestEventId = part.id; @@ -1083,7 +1079,8 @@ function toNodeListener( res.setHeader(key, value); }); const setCookies = - typeof (webRes.headers as Headers & { getSetCookie?: () => string[] }).getSetCookie === "function" + typeof (webRes.headers as Headers & { getSetCookie?: () => string[] }).getSetCookie === + "function" ? (webRes.headers as Headers & { getSetCookie: () => string[] }).getSetCookie() : []; if (setCookies.length > 0) { diff --git a/packages/trigger-sdk/src/v3/chat.test.ts b/packages/trigger-sdk/src/v3/chat.test.ts index e4fc45eae4..50bfb5e409 100644 --- a/packages/trigger-sdk/src/v3/chat.test.ts +++ b/packages/trigger-sdk/src/v3/chat.test.ts @@ -173,22 +173,17 @@ function defaultSseResponse( } function authError(status = 401): Response { - return new Response( - JSON.stringify({ error: "Unauthorized", name: "TriggerApiError", status }), - { - status, - headers: { "content-type": "application/json" }, - } - ); + return new Response(JSON.stringify({ error: "Unauthorized", name: "TriggerApiError", status }), { + status, + headers: { "content-type": "application/json" }, + }); } /** * Drains a UIMessageChunk stream into an array. Used to assert what * the transport surfaced after filtering control chunks. */ -async function drainChunks( - stream: ReadableStream -): Promise { +async function drainChunks(stream: ReadableStream): Promise { const reader = stream.getReader(); const out: UIMessageChunk[] = []; try { @@ -331,9 +326,7 @@ describe("TriggerChatTransport", () => { }); it("is idempotent β€” second call returns the cached state without re-invoking startSession", async () => { - const startSession = vi - .fn() - .mockResolvedValue({ publicAccessToken: "session-pat-2" }); + const startSession = vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-2" }); const transport = new TriggerChatTransport({ task: "my-chat-task", @@ -369,9 +362,7 @@ describe("TriggerChatTransport", () => { }); it("preload() is an alias for start()", async () => { - const startSession = vi - .fn() - .mockResolvedValue({ publicAccessToken: "session-pat-pre" }); + const startSession = vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-pre" }); const transport = new TriggerChatTransport({ task: "my-chat-task", @@ -393,9 +384,7 @@ describe("TriggerChatTransport", () => { }); it("threads the transport's `clientData` through to startSession", async () => { - const startSession = vi - .fn() - .mockResolvedValue({ publicAccessToken: "session-pat-cd" }); + const startSession = vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-cd" }); const transport = new TriggerChatTransport({ task: "my-chat-task", @@ -414,9 +403,7 @@ describe("TriggerChatTransport", () => { }); it("setClientData updates the value passed to subsequent startSession calls", async () => { - const startSession = vi - .fn() - .mockResolvedValue({ publicAccessToken: "session-pat-set" }); + const startSession = vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-set" }); const transport = new TriggerChatTransport({ task: "my-chat-task", @@ -438,9 +425,7 @@ describe("TriggerChatTransport", () => { describe("ensureSessionState (lazy start on first sendMessage)", () => { it("calls startSession lazily on first sendMessage when no PAT is hydrated", async () => { - const startSession = vi - .fn() - .mockResolvedValue({ publicAccessToken: "lazy-session-pat" }); + const startSession = vi.fn().mockResolvedValue({ publicAccessToken: "lazy-session-pat" }); global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { const urlStr = typeof url === "string" ? url : url.toString(); @@ -565,8 +550,8 @@ describe("TriggerChatTransport", () => { expect(chunks).toHaveLength(sampleChunks.length); expect(chunks[0]).toEqual(sampleChunks[0]); - const append = requests.find((r) => - isSessionStreamAppendUrl(r.url) && r.url.endsWith("/in/append") + const append = requests.find( + (r) => isSessionStreamAppendUrl(r.url) && r.url.endsWith("/in/append") ); expect(append).toBeDefined(); expect(chatIdFromUrl(append!.url)).toBe("chat-1"); @@ -620,9 +605,7 @@ describe("TriggerChatTransport", () => { }); const baseURLFn = vi.fn(({ endpoint }: { endpoint: "in" | "out"; chatId: string }) => - endpoint === "out" - ? "https://stream.example.com" - : "https://api.example.com" + endpoint === "out" ? "https://stream.example.com" : "https://api.example.com" ); const transport = new TriggerChatTransport({ @@ -658,11 +641,7 @@ describe("TriggerChatTransport", () => { const fetchCalls: Array<{ url: string; endpoint: string; chatId: string }> = []; const customFetch = vi.fn( - async ( - url: string, - init: RequestInit, - ctx: { endpoint: "in" | "out"; chatId: string } - ) => { + async (url: string, init: RequestInit, ctx: { endpoint: "in" | "out"; chatId: string }) => { fetchCalls.push({ url, endpoint: ctx.endpoint, chatId: ctx.chatId }); if (isSessionStreamAppendUrl(url)) return defaultAppendResponse(); if (isSessionOutSubscribeUrl(url)) return defaultSseResponse(); @@ -1194,9 +1173,7 @@ describe("TriggerChatTransport", () => { // Only the endpoint was called β€” no /api/v1/sessions, no .in/append, // no .out subscribe. The handler owns first-turn end-to-end. - const endpointPosts = requests.filter( - (r) => r.url === "https://my-app.example/api/chat" - ); + const endpointPosts = requests.filter((r) => r.url === "https://my-app.example/api/chat"); expect(endpointPosts).toHaveLength(1); expect(requests.some((r) => isSessionCreateUrl(r.url))).toBe(false); expect(requests.some((r) => isSessionStreamAppendUrl(r.url))).toBe(false); diff --git a/packages/trigger-sdk/src/v3/chat.ts b/packages/trigger-sdk/src/v3/chat.ts index e0d37ad7d4..d7a71ee842 100644 --- a/packages/trigger-sdk/src/v3/chat.ts +++ b/packages/trigger-sdk/src/v3/chat.ts @@ -462,9 +462,10 @@ export class TriggerChatTransport implements ChatTransport { | undefined; const baseURLOption = options.baseURL ?? DEFAULT_BASE_URL; const streamOverride = options.streamBaseURL; - this.resolveBaseURLFn = typeof baseURLOption === "function" - ? (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption(ctx)) - : (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption); + this.resolveBaseURLFn = + typeof baseURLOption === "function" + ? (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption(ctx)) + : (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption); this.fetchOverride = options.fetch; this.extraHeaders = options.headers ?? {}; this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS; @@ -687,9 +688,7 @@ export class TriggerChatTransport implements ChatTransport { }); if (!response.ok) { - throw new Error( - `chat.handover endpoint returned ${response.status} ${response.statusText}` - ); + throw new Error(`chat.handover endpoint returned ${response.status} ${response.statusText}`); } if (!response.body) { throw new Error("chat.handover endpoint returned no response body"); @@ -906,10 +905,7 @@ export class TriggerChatTransport implements ChatTransport { * `StreamTextResult`); for `void`-returning side-effect-only actions * the stream completes immediately with `trigger:turn-complete`. */ - sendAction = async ( - chatId: string, - action: unknown - ): Promise> => { + sendAction = async (chatId: string, action: unknown): Promise> => { if (this.coordinator) { if (this.coordinator.isReadOnly(chatId)) { throw new Error("This chat is active in another tab"); @@ -1320,7 +1316,7 @@ export class TriggerChatTransport implements ChatTransport { const sseCtx: ChatTransportEndpointContext = { endpoint: "out", chatId }; const fetchOverride = this.fetchOverride; const sseFetchClient: typeof fetch | undefined = fetchOverride - ? ((input, init) => { + ? (((input, init) => { if (typeof input === "string") { return fetchOverride(input, init ?? {}, sseCtx); } @@ -1340,7 +1336,7 @@ export class TriggerChatTransport implements ChatTransport { }, sseCtx ); - }) as typeof fetch + }) as typeof fetch) : undefined; const connectSseOnce = async (token: string) => { const subscription = new SSEStreamSubscription(streamUrl, { @@ -1448,9 +1444,7 @@ export class TriggerChatTransport implements ChatTransport { // when no header is present so the deploy-skew window // closes turns correctly. let controlValue = controlSubtype(value.headers); - let legacyChunk: - | { type?: string; publicAccessToken?: string } - | undefined; + let legacyChunk: { type?: string; publicAccessToken?: string } | undefined; if (!controlValue && value.chunk && typeof value.chunk === "object") { const chunk = value.chunk as { type?: unknown; publicAccessToken?: unknown }; if (chunk.type === "trigger:turn-complete") { diff --git a/packages/trigger-sdk/src/v3/idempotencyKeys.ts b/packages/trigger-sdk/src/v3/idempotencyKeys.ts index 0030dbf3aa..1b0e31ec3a 100644 --- a/packages/trigger-sdk/src/v3/idempotencyKeys.ts +++ b/packages/trigger-sdk/src/v3/idempotencyKeys.ts @@ -1,4 +1,8 @@ -import { createIdempotencyKey, resetIdempotencyKey, type IdempotencyKey } from "@trigger.dev/core/v3"; +import { + createIdempotencyKey, + resetIdempotencyKey, + type IdempotencyKey, +} from "@trigger.dev/core/v3"; export const idempotencyKeys = { create: createIdempotencyKey, diff --git a/packages/trigger-sdk/src/v3/prompt.ts b/packages/trigger-sdk/src/v3/prompt.ts index 1c465b1ee4..2b5892a5f8 100644 --- a/packages/trigger-sdk/src/v3/prompt.ts +++ b/packages/trigger-sdk/src/v3/prompt.ts @@ -55,34 +55,28 @@ export type PromptHandle< export type AnyPromptHandle = PromptHandle; /** Extract the identifier (id literal type) from a PromptHandle */ -export type PromptIdentifier = T extends PromptHandle - ? TId - : string; +export type PromptIdentifier = + T extends PromptHandle ? TId : string; /** Extract the variables input type from a PromptHandle */ -export type PromptVariables = T extends PromptHandle - ? inferSchemaIn - : Record; +export type PromptVariables = + T extends PromptHandle + ? inferSchemaIn + : Record; /** * Compile a Mustache-style template by substituting `{{variable}}` placeholders. */ -function compileTemplate( - template: string, - variables: Record -): string { +function compileTemplate(template: string, variables: Record): string { // Handle conditional sections: {{#key}}...{{/key}} - let result = template.replace( - /\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, - (_match, key, content) => { - const value = variables[key]; - return value - ? content.replace(/\{\{(\w+)\}\}/g, (_m: string, k: string) => { - return String(variables[k] ?? ""); - }) - : ""; - } - ); + let result = template.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_match, key, content) => { + const value = variables[key]; + return value + ? content.replace(/\{\{(\w+)\}\}/g, (_m: string, k: string) => { + return String(variables[k] ?? ""); + }) + : ""; + }); // Handle simple substitutions: {{key}} result = result.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => { @@ -124,7 +118,14 @@ function resolveLocally( variables: Record ): ResolvedPrompt { const inputJson = Object.keys(variables).length > 0 ? JSON.stringify(variables) : undefined; - const telemetryFn = makeToAISDKTelemetry(options.id, options.id, 0, ["local"], options.model, inputJson); + const telemetryFn = makeToAISDKTelemetry( + options.id, + options.id, + 0, + ["local"], + options.model, + inputJson + ); return { promptId: options.id, @@ -140,12 +141,8 @@ function resolveLocally( export function definePrompt< TIdentifier extends string, TVariables extends TaskSchema | undefined = undefined, ->( - options: PromptOptions -): PromptHandle { - const parseVariables = options.variables - ? getSchemaParseFn(options.variables) - : undefined; +>(options: PromptOptions): PromptHandle { + const parseVariables = options.variables ? getSchemaParseFn(options.variables) : undefined; // Register with resource catalog const metadata: PromptMetadataWithFunctions = { @@ -170,9 +167,7 @@ export function definePrompt< id: options.id, resolve: async (variables, resolveOptions) => { // Validate variables if schema provided - const validated = parseVariables - ? await parseVariables(variables) - : variables; + const validated = parseVariables ? await parseVariables(variables) : variables; const vars = validated as Record; const apiClient = apiClientManager.client; diff --git a/packages/trigger-sdk/src/v3/promptManagement.ts b/packages/trigger-sdk/src/v3/promptManagement.ts index 480676f445..b9876d70e4 100644 --- a/packages/trigger-sdk/src/v3/promptManagement.ts +++ b/packages/trigger-sdk/src/v3/promptManagement.ts @@ -10,7 +10,12 @@ import { type PromptOverrideCreatedResponseBody, type UpdatePromptOverrideRequestBody, } from "@trigger.dev/core/v3"; -import type { AnyPromptHandle, PromptIdentifier, PromptVariables, ResolvedPrompt } from "./prompt.js"; +import type { + AnyPromptHandle, + PromptIdentifier, + PromptVariables, + ResolvedPrompt, +} from "./prompt.js"; import { tracer } from "./tracer.js"; function promptSpanOptions(name: string, slug: string) { @@ -119,9 +124,7 @@ export async function resolvePrompt { +export function listPrompts(requestOptions?: ApiRequestOptions): Promise { const apiClient = apiClientManager.clientOrThrow(); return apiClient.listPrompts({ tracer, @@ -158,14 +161,18 @@ export async function promotePromptVersion( requestOptions?: ApiRequestOptions ): Promise { const apiClient = apiClientManager.clientOrThrow(); - return apiClient.promotePromptVersion(slug, { version }, { - ...promptSpanOptions("prompts.promote()", slug), - attributes: { - ...promptSpanOptions("prompts.promote()", slug).attributes, - "prompt.slug": slug, - "prompt.version": version, - }, - }); + return apiClient.promotePromptVersion( + slug, + { version }, + { + ...promptSpanOptions("prompts.promote()", slug), + attributes: { + ...promptSpanOptions("prompts.promote()", slug).attributes, + "prompt.slug": slug, + "prompt.version": version, + }, + } + ); } /** Create an override β€” a dashboard/API edit that takes priority over the deployed version. */ @@ -226,12 +233,16 @@ export async function reactivatePromptOverride( requestOptions?: ApiRequestOptions ): Promise { const apiClient = apiClientManager.clientOrThrow(); - return apiClient.reactivatePromptOverride(slug, { version }, { - ...promptSpanOptions("prompts.reactivateOverride()", slug), - attributes: { - ...promptSpanOptions("prompts.reactivateOverride()", slug).attributes, - "prompt.slug": slug, - "prompt.version": version, - }, - }); + return apiClient.reactivatePromptOverride( + slug, + { version }, + { + ...promptSpanOptions("prompts.reactivateOverride()", slug), + attributes: { + ...promptSpanOptions("prompts.reactivateOverride()", slug).attributes, + "prompt.slug": slug, + "prompt.version": version, + }, + } + ); } diff --git a/packages/trigger-sdk/src/v3/retry.ts b/packages/trigger-sdk/src/v3/retry.ts index 65e3203f37..c70794c21f 100644 --- a/packages/trigger-sdk/src/v3/retry.ts +++ b/packages/trigger-sdk/src/v3/retry.ts @@ -164,12 +164,9 @@ async function retryFetch( const abortController = new AbortController(); const timeoutId = init?.timeoutInMs - ? setTimeout( - () => { - abortController.abort(); - }, - init?.timeoutInMs - ) + ? setTimeout(() => { + abortController.abort(); + }, init?.timeoutInMs) : undefined; init?.signal?.addEventListener("abort", () => { diff --git a/packages/trigger-sdk/src/v3/runs.ts b/packages/trigger-sdk/src/v3/runs.ts index 88e6d2b701..db949c876b 100644 --- a/packages/trigger-sdk/src/v3/runs.ts +++ b/packages/trigger-sdk/src/v3/runs.ts @@ -160,10 +160,10 @@ function listRunsRequestOptions( type RunId = TRunId extends AnyRunHandle | AnyBatchedRunHandle ? TRunId : TRunId extends AnyTask - ? string - : TRunId extends string - ? TRunId - : never; + ? string + : TRunId extends string + ? TRunId + : never; function retrieveRun( runId: RunId, diff --git a/packages/trigger-sdk/src/v3/schedules/index.ts b/packages/trigger-sdk/src/v3/schedules/index.ts index 1f87ed83e2..cd7bd69141 100644 --- a/packages/trigger-sdk/src/v3/schedules/index.ts +++ b/packages/trigger-sdk/src/v3/schedules/index.ts @@ -46,13 +46,13 @@ export type ScheduleOptions< timezone?: string; /** You can optionally specify which environments this schedule should run in. * When not specified, the schedule will run in all environments. - * + * * @example * ```ts * environments: ["PRODUCTION", "STAGING"] * ``` - * - * @example + * + * @example * ```ts * environments: ["PRODUCTION"] // Only run in production * ``` diff --git a/packages/trigger-sdk/src/v3/schemas.ts b/packages/trigger-sdk/src/v3/schemas.ts index 65be53024e..44898a17c2 100644 --- a/packages/trigger-sdk/src/v3/schemas.ts +++ b/packages/trigger-sdk/src/v3/schemas.ts @@ -1,2 +1,2 @@ // Re-export JSON Schema types for user convenience -export type { JSONSchema } from "@trigger.dev/core/v3"; \ No newline at end of file +export type { JSONSchema } from "@trigger.dev/core/v3"; diff --git a/packages/trigger-sdk/src/v3/sessions.ts b/packages/trigger-sdk/src/v3/sessions.ts index d2a33c09fe..1a7c7716c9 100644 --- a/packages/trigger-sdk/src/v3/sessions.ts +++ b/packages/trigger-sdk/src/v3/sessions.ts @@ -409,16 +409,13 @@ export class SessionOutputChannel { * shared {@link SSEStreamSubscription} plumbing used by run-scoped * realtime streams. */ - async read( - options?: SessionSubscribeOptions - ): Promise> { + async read(options?: SessionSubscribeOptions): Promise> { const apiClient = apiClientManager.clientOrThrow(); return apiClient.subscribeToSessionStream(this.sessionId, "out", { signal: options?.signal, timeoutInSeconds: options?.timeoutInSeconds, - lastEventId: - options?.lastEventId != null ? String(options.lastEventId) : undefined, + lastEventId: options?.lastEventId != null ? String(options.lastEventId) : undefined, onPart: options?.onPart, onControl: options?.onControl, onComplete: options?.onComplete, @@ -827,9 +824,7 @@ export class SessionInputChannel { span.setAttribute("wait.resolved", "skipped"); return { ok: false as const, - error: new WaitpointTimeoutError( - "Idle timeout elapsed and skipSuspend is set" - ), + error: new WaitpointTimeoutError("Idle timeout elapsed and skipSuspend is set"), }; } diff --git a/packages/trigger-sdk/src/v3/shared.test.ts b/packages/trigger-sdk/src/v3/shared.test.ts index 4a0509b6b3..16191e9826 100644 --- a/packages/trigger-sdk/src/v3/shared.test.ts +++ b/packages/trigger-sdk/src/v3/shared.test.ts @@ -216,4 +216,3 @@ describe("readableStreamToAsyncIterable", () => { expect(producedValues.length).toBeLessThanOrEqual(3); }); }); - diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index fba990949b..592a4b62df 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -1038,7 +1038,10 @@ export async function batchTriggerByIdAndWait( ctx, }); - const runs = await handleBatchTaskRunExecutionResultV2(result.items, response.taskIdentifiers); + const runs = await handleBatchTaskRunExecutionResultV2( + result.items, + response.taskIdentifiers + ); return { id: result.id, @@ -1082,7 +1085,10 @@ export async function batchTriggerByIdAndWait( ctx, }); - const runs = await handleBatchTaskRunExecutionResultV2(result.items, response.taskIdentifiers); + const runs = await handleBatchTaskRunExecutionResultV2( + result.items, + response.taskIdentifiers + ); return { id: result.id, @@ -1559,7 +1565,10 @@ export async function batchTriggerAndWaitTasks( queue: item.options?.queue ? { name: item.options.queue } : queue - ? { name: queue } - : undefined, + ? { name: queue } + : undefined, concurrencyKey: item.options?.concurrencyKey, test: taskContext.ctx?.run.isTest, payloadType: payloadPacket.dataType, @@ -2181,8 +2193,8 @@ async function* transformSingleTaskBatchItemsStreamForWait( queue: item.options?.queue ? { name: item.options.queue } : queue - ? { name: queue } - : undefined, + ? { name: queue } + : undefined, concurrencyKey: item.options?.concurrencyKey, test: taskContext.ctx?.run.isTest, payloadType: payloadPacket.dataType, @@ -2314,8 +2326,8 @@ async function batchTrigger_internal( queue: item.options?.queue ? { name: item.options.queue } : queue - ? { name: queue } - : undefined, + ? { name: queue } + : undefined, concurrencyKey: item.options?.concurrencyKey, test: taskContext.ctx?.run.isTest, payloadType: payloadPacket.dataType, @@ -2736,8 +2748,8 @@ async function batchTriggerAndWait_internal = { export type AnySkillHandle = SkillHandle; /** Extract the id literal type from a SkillHandle. */ -export type SkillIdentifier = T extends SkillHandle - ? TId - : string; +export type SkillIdentifier = + T extends SkillHandle ? TId : string; /** * Bundled skills are copied to `${cwd}/.trigger/skills/{id}/` by the CLI at diff --git a/packages/trigger-sdk/src/v3/streams.test.ts b/packages/trigger-sdk/src/v3/streams.test.ts index 67ee00fd26..5c22330f24 100644 --- a/packages/trigger-sdk/src/v3/streams.test.ts +++ b/packages/trigger-sdk/src/v3/streams.test.ts @@ -3,62 +3,62 @@ import { streams } from "./streams.js"; import { taskContext, realtimeStreams } from "@trigger.dev/core/v3"; vi.mock("@trigger.dev/core/v3", async (importOriginal) => { - const original = await importOriginal(); - return { - ...original, - taskContext: { - ctx: { - run: { - id: "run_123", - // parentTaskRunId and rootTaskRunId are undefined for root tasks - }, - }, + const original = await importOriginal(); + return { + ...original, + taskContext: { + ctx: { + run: { + id: "run_123", + // parentTaskRunId and rootTaskRunId are undefined for root tasks }, - realtimeStreams: { - pipe: vi.fn().mockReturnValue({ - wait: () => Promise.resolve(), - stream: new ReadableStream(), - }), - }, - }; + }, + }, + realtimeStreams: { + pipe: vi.fn().mockReturnValue({ + wait: () => Promise.resolve(), + stream: new ReadableStream(), + }), + }, + }; }); describe("streams.pipe consistency", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); + beforeEach(() => { + vi.clearAllMocks(); + }); - it("should not throw and should use self runId when target is 'root' in a root task", async () => { - const mockStream = new ReadableStream(); + it("should not throw and should use self runId when target is 'root' in a root task", async () => { + const mockStream = new ReadableStream(); - // This should not throw anymore - const { waitUntilComplete } = streams.pipe("test-key", mockStream, { - target: "root", - }); - - expect(realtimeStreams.pipe).toHaveBeenCalledWith( - "test-key", - mockStream, - expect.objectContaining({ - target: "run_123", - }) - ); + // This should not throw anymore + const { waitUntilComplete } = streams.pipe("test-key", mockStream, { + target: "root", }); - it("should not throw and should use self runId when target is 'parent' in a root task", async () => { - const mockStream = new ReadableStream(); + expect(realtimeStreams.pipe).toHaveBeenCalledWith( + "test-key", + mockStream, + expect.objectContaining({ + target: "run_123", + }) + ); + }); - // This should not throw anymore - const { waitUntilComplete } = streams.pipe("test-key", mockStream, { - target: "parent", - }); + it("should not throw and should use self runId when target is 'parent' in a root task", async () => { + const mockStream = new ReadableStream(); - expect(realtimeStreams.pipe).toHaveBeenCalledWith( - "test-key", - mockStream, - expect.objectContaining({ - target: "run_123", - }) - ); + // This should not throw anymore + const { waitUntilComplete } = streams.pipe("test-key", mockStream, { + target: "parent", }); + + expect(realtimeStreams.pipe).toHaveBeenCalledWith( + "test-key", + mockStream, + expect.objectContaining({ + target: "run_123", + }) + ); + }); }); diff --git a/packages/trigger-sdk/src/v3/streams.ts b/packages/trigger-sdk/src/v3/streams.ts index f987872d80..57d08e8055 100644 --- a/packages/trigger-sdk/src/v3/streams.ts +++ b/packages/trigger-sdk/src/v3/streams.ts @@ -751,10 +751,7 @@ function input(opts: { id: string }): RealtimeDefinedInputStream { return { id: opts.id, on(handler) { - return inputStreams.on( - opts.id, - handler as (data: unknown) => void | Promise - ); + return inputStreams.on(opts.id, handler as (data: unknown) => void | Promise); }, once(options) { const ctx = taskContext.ctx; @@ -774,9 +771,7 @@ function input(opts: { id: string }): RealtimeDefinedInputStream { attributes: { [SemanticInternalAttributes.STYLE_ICON]: "streams", [SemanticInternalAttributes.ENTITY_TYPE]: "input-stream", - ...(runId - ? { [SemanticInternalAttributes.ENTITY_ID]: `${runId}:${opts.id}` } - : {}), + ...(runId ? { [SemanticInternalAttributes.ENTITY_ID]: `${runId}:${opts.id}` } : {}), streamId: opts.id, ...accessoryAttributes({ items: [{ text: opts.id, variant: "normal" }], @@ -815,7 +810,6 @@ function input(opts: { id: string }): RealtimeDefinedInputStream { const result = await tracer.startActiveSpan( options?.spanName ?? `inputStream.wait()`, async (span) => { - // 1. Block the run on the waitpoint const waitResponse = await apiClient.waitForWaitpointToken({ runFriendlyId: ctx.run.id, @@ -839,12 +833,12 @@ function input(opts: { id: string }): RealtimeDefinedInputStream { const data = waitResult.output !== undefined ? await conditionallyImportAndParsePacket( - { - data: waitResult.output, - dataType: waitResult.outputType ?? "application/json", - }, - apiClient - ) + { + data: waitResult.output, + dataType: waitResult.outputType ?? "application/json", + }, + apiClient + ) : undefined; if (waitResult.ok) { @@ -916,9 +910,7 @@ function input(opts: { id: string }): RealtimeDefinedInputStream { span.setAttribute("wait.resolved", "skipped"); return { ok: false as const, - error: new WaitpointTimeoutError( - "Idle timeout elapsed and skipSuspend is set" - ), + error: new WaitpointTimeoutError("Idle timeout elapsed and skipSuspend is set"), }; } 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 70eae4696d..a4cbe7b33a 100644 --- a/packages/trigger-sdk/src/v3/test/mock-chat-agent.ts +++ b/packages/trigger-sdk/src/v3/test/mock-chat-agent.ts @@ -1,14 +1,8 @@ import type { UIMessage, UIMessageChunk } from "ai"; import { resourceCatalog } from "@trigger.dev/core/v3"; import type { LocalsKey } from "@trigger.dev/core/v3"; -import { - runInMockTaskContext, - type MockTaskContextOptions, -} from "@trigger.dev/core/v3/test"; -import { - __setSessionOpenImplForTests, - __setSessionStartImplForTests, -} from "../sessions.js"; +import { runInMockTaskContext, type MockTaskContextOptions } from "@trigger.dev/core/v3/test"; +import { __setSessionOpenImplForTests, __setSessionStartImplForTests } from "../sessions.js"; import { __setReadChatSnapshotImplForTests, __setReplaySessionInTailImplForTests, @@ -16,10 +10,7 @@ import { __setWriteChatSnapshotImplForTests, type ChatSnapshotV1, } from "../ai.js"; -import { - createTestSessionHandle, - type TestSessionOutState, -} from "./test-session-handle.js"; +import { createTestSessionHandle, type TestSessionOutState } from "./test-session-handle.js"; /** Pre-seed locals before the agent's `run()` starts. */ export type SetupLocals = (locals: { @@ -287,10 +278,7 @@ export type MockChatAgentHarness = { readonly allRawChunks: unknown[]; }; -const CONTROL_CHUNK_TYPES = new Set([ - "trigger:turn-complete", - "trigger:upgrade-required", -]); +const CONTROL_CHUNK_TYPES = new Set(["trigger:turn-complete", "trigger:upgrade-required"]); function isControlChunk(chunk: unknown): boolean { if (typeof chunk !== "object" || chunk === null) return false; @@ -407,9 +395,11 @@ export function mockChatAgent( __setReadChatSnapshotImplForTests((_id: string) => { return seededSnapshot as ChatSnapshotV1 | undefined; }); - __setWriteChatSnapshotImplForTests((_id: string, snapshot: ChatSnapshotV1) => { - lastWrittenSnapshot = snapshot as ChatSnapshotV1; - }); + __setWriteChatSnapshotImplForTests( + (_id: string, snapshot: ChatSnapshotV1) => { + lastWrittenSnapshot = snapshot as ChatSnapshotV1; + } + ); // Replay override: install a default that returns whatever // `seededReplayChunks` reduces to. `mockChatAgent` doesn't model the @@ -487,83 +477,80 @@ export function mockChatAgent( }; }); - taskFinished = runInMockTaskContext( - async (drivers) => { - runSignal = new AbortController(); - - // For `mode: "continuation"`, omit `trigger` from the wire payload β€” - // mirrors what the server's `ensureRunForSession` / `swapSessionRun` - // produces (the continuation overrides clear `trigger` so the SDK - // boot path falls into the continuation-wait branch instead of - // re-firing the basePayload's stale first-run trigger). `continuation: - // true` is set unconditionally for this mode so the boot path's - // continuation-wait condition matches. - const isContinuationMode = mode === "continuation"; - const initialPayload: ChatWirePayload = { - chatId, - ...(isContinuationMode - ? { trigger: undefined as never, continuation: true } - : { trigger: mode }), - metadata: clientData, - ...(!isContinuationMode && options.continuation ? { continuation: true } : {}), - ...(options.previousRunId ? { previousRunId: options.previousRunId } : {}), - ...(options.headStartMessages ? { headStartMessages: options.headStartMessages } : {}), - }; - - sendSessionInput = drivers.sessions.in.send; - closeSessionInput = drivers.sessions.in.close; - - // Record every chunk written to session.out, detect turn-complete. - const listener = (chunk: unknown) => { - allRawChunks.push(chunk); - if (!isControlChunk(chunk)) { - allChunks.push(chunk as UIMessageChunk); - } - if ( - typeof chunk === "object" && - chunk !== null && - (chunk as { type?: string }).type === "trigger:turn-complete" - ) { - const resolvers = turnCompleteResolvers; - turnCompleteResolvers = []; - for (const resolve of resolvers) resolve(); - } - }; - sessionOutState.listeners.add(listener); - const unsubscribe = () => sessionOutState.listeners.delete(listener); - - if (options.setupLocals) { - await options.setupLocals({ set: drivers.locals.set }); - } + taskFinished = runInMockTaskContext(async (drivers) => { + runSignal = new AbortController(); + + // For `mode: "continuation"`, omit `trigger` from the wire payload β€” + // mirrors what the server's `ensureRunForSession` / `swapSessionRun` + // produces (the continuation overrides clear `trigger` so the SDK + // boot path falls into the continuation-wait branch instead of + // re-firing the basePayload's stale first-run trigger). `continuation: + // true` is set unconditionally for this mode so the boot path's + // continuation-wait condition matches. + const isContinuationMode = mode === "continuation"; + const initialPayload: ChatWirePayload = { + chatId, + ...(isContinuationMode + ? { trigger: undefined as never, continuation: true } + : { trigger: mode }), + metadata: clientData, + ...(!isContinuationMode && options.continuation ? { continuation: true } : {}), + ...(options.previousRunId ? { previousRunId: options.previousRunId } : {}), + ...(options.headStartMessages ? { headStartMessages: options.headStartMessages } : {}), + }; - harnessReadyResolve(); + sendSessionInput = drivers.sessions.in.send; + closeSessionInput = drivers.sessions.in.close; - try { - if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { - console.log("[mockChatAgent] Starting runFn with payload:", initialPayload); - } - await runFn(initialPayload, { - ctx: drivers.ctx, - signal: runSignal.signal, - }); - if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { - console.log("[mockChatAgent] runFn returned"); - } - } catch (err) { - if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { - console.log("[mockChatAgent] runFn threw:", err); - } - throw err; - } finally { - unsubscribe(); - // Resolve any outstanding turn-complete waiters so callers don't hang + // Record every chunk written to session.out, detect turn-complete. + const listener = (chunk: unknown) => { + allRawChunks.push(chunk); + if (!isControlChunk(chunk)) { + allChunks.push(chunk as UIMessageChunk); + } + if ( + typeof chunk === "object" && + chunk !== null && + (chunk as { type?: string }).type === "trigger:turn-complete" + ) { const resolvers = turnCompleteResolvers; turnCompleteResolvers = []; for (const resolve of resolvers) resolve(); } - }, - options.taskContext - ) + }; + sessionOutState.listeners.add(listener); + const unsubscribe = () => sessionOutState.listeners.delete(listener); + + if (options.setupLocals) { + await options.setupLocals({ set: drivers.locals.set }); + } + + harnessReadyResolve(); + + try { + if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { + console.log("[mockChatAgent] Starting runFn with payload:", initialPayload); + } + await runFn(initialPayload, { + ctx: drivers.ctx, + signal: runSignal.signal, + }); + if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { + console.log("[mockChatAgent] runFn returned"); + } + } catch (err) { + if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { + console.log("[mockChatAgent] runFn threw:", err); + } + throw err; + } finally { + unsubscribe(); + // Resolve any outstanding turn-complete waiters so callers don't hang + const resolvers = turnCompleteResolvers; + turnCompleteResolvers = []; + for (const resolve of resolvers) resolve(); + } + }, options.taskContext) .catch((err) => { // Propagate errors to pending turn waiters instead of dropping them const resolvers = turnCompleteResolvers; @@ -581,18 +568,14 @@ export function mockChatAgent( __setReplaySessionInTailImplForTests(undefined); }); - const sendPayloadAndWait = async ( - payload: ChatWirePayload - ): Promise => { + const sendPayloadAndWait = async (payload: ChatWirePayload): Promise => { await harnessReady; const before = allRawChunks.length; const turnComplete = waitForTurnComplete(); await sendSessionInput(sessionId, { kind: "message", payload }); await turnComplete; const rawChunks = allRawChunks.slice(before); - const chunks = rawChunks.filter( - (c) => !isControlChunk(c) - ) as UIMessageChunk[]; + const chunks = rawChunks.filter((c) => !isControlChunk(c)) as UIMessageChunk[]; return { chunks, rawChunks }; }; @@ -745,7 +728,9 @@ export function mockChatAgent( async function reduceChunksToMessages(chunks: UIMessageChunk[]): Promise { if (chunks.length === 0) return []; const aiModule = (await import("ai")) as { - readUIMessageStream?: (args: { stream: ReadableStream }) => AsyncIterable; + readUIMessageStream?: (args: { + stream: ReadableStream; + }) => AsyncIterable; cleanupAbortedParts?: (msg: UIMessage) => UIMessage; }; const readUIMessageStream = aiModule.readUIMessageStream; 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 93d6146ddd..3e36f402a8 100644 --- a/packages/trigger-sdk/src/v3/test/test-session-handle.ts +++ b/packages/trigger-sdk/src/v3/test/test-session-handle.ts @@ -27,7 +27,10 @@ import { * network call. */ class TestSessionInputChannel extends SessionInputChannel { - constructor(sessionId: string, private readonly getAbortSignal: () => AbortSignal | undefined) { + constructor( + sessionId: string, + private readonly getAbortSignal: () => AbortSignal | undefined + ) { super(sessionId); } @@ -35,26 +38,28 @@ class TestSessionInputChannel extends SessionInputChannel { // continue to flow through the real `sessionStreams` global, which // the mock task context installs as a `TestSessionStreamManager`. wait(): ManualWaitpointPromise { - return new ManualWaitpointPromise((resolve: (value: { ok: false; error: Error }) => void) => { - const signal = this.getAbortSignal(); - if (!signal) { - // Harness hasn't wired up its run signal yet β€” nothing to abort - // on. Stay pending; the run loop should never reach this state - // in practice but we don't want to throw here either. - return; - } - const onAbort = () => { - resolve({ - ok: false, - error: new Error("session.in.wait() aborted by test harness"), - }); - }; - if (signal.aborted) { - onAbort(); - return; + return new ManualWaitpointPromise( + (resolve: (value: { ok: false; error: Error }) => void) => { + const signal = this.getAbortSignal(); + if (!signal) { + // Harness hasn't wired up its run signal yet β€” nothing to abort + // on. Stay pending; the run loop should never reach this state + // in practice but we don't want to throw here either. + return; + } + const onAbort = () => { + resolve({ + ok: false, + error: new Error("session.in.wait() aborted by test harness"), + }); + }; + if (signal.aborted) { + onAbort(); + return; + } + signal.addEventListener("abort", onAbort, { once: true }); } - signal.addEventListener("abort", onAbort, { once: true }); - }); + ); } } @@ -198,9 +203,7 @@ export class TestSessionOutputChannel extends SessionOutputChannel { notify(state, part); }, merge(streamArg) { - ongoing.push( - drainInto(streamArg, state).catch(() => {}) - ); + ongoing.push(drainInto(streamArg, state).catch(() => {})); }, }); diff --git a/packages/trigger-sdk/src/v3/triggerClient.test.ts b/packages/trigger-sdk/src/v3/triggerClient.test.ts index f5374171ed..a19502a08f 100644 --- a/packages/trigger-sdk/src/v3/triggerClient.test.ts +++ b/packages/trigger-sdk/src/v3/triggerClient.test.ts @@ -15,7 +15,7 @@ function installFetchSpy() { const originalFetch = globalThis.fetch; globalThis.fetch = (async (input: any, init?: RequestInit) => { - const url = typeof input === "string" ? input : input?.url ?? String(input); + const url = typeof input === "string" ? input : (input?.url ?? String(input)); const headers = new Headers(init?.headers); captured.push({ url, diff --git a/packages/trigger-sdk/test/chat-snapshot.test.ts b/packages/trigger-sdk/test/chat-snapshot.test.ts index 85e1fe8ade..6ae6c3891f 100644 --- a/packages/trigger-sdk/test/chat-snapshot.test.ts +++ b/packages/trigger-sdk/test/chat-snapshot.test.ts @@ -88,11 +88,12 @@ describe("chat snapshot helpers", () => { it("returns the snapshot on a successful GET", async () => { const { getChatSnapshotUrl } = stubApiClient({}); const snapshot = buildSnapshot(2); - stubFetch(async () => - new Response(JSON.stringify(snapshot), { - status: 200, - headers: { "content-type": "application/json" }, - }) + stubFetch( + async () => + new Response(JSON.stringify(snapshot), { + status: 200, + headers: { "content-type": "application/json" }, + }) ); const result = await readChatSnapshot("session-1"); @@ -122,11 +123,12 @@ describe("chat snapshot helpers", () => { it("returns undefined when the response body is malformed JSON", async () => { stubApiClient({}); - stubFetch(async () => - new Response("not-json-{[", { - status: 200, - headers: { "content-type": "application/json" }, - }) + stubFetch( + async () => + new Response("not-json-{[", { + status: 200, + headers: { "content-type": "application/json" }, + }) ); const result = await readChatSnapshot("malformed-session"); @@ -141,11 +143,12 @@ describe("chat snapshot helpers", () => { savedAt: Date.now(), messages: [], }; - stubFetch(async () => - new Response(JSON.stringify(futureSnapshot), { - status: 200, - headers: { "content-type": "application/json" }, - }) + stubFetch( + async () => + new Response(JSON.stringify(futureSnapshot), { + status: 200, + headers: { "content-type": "application/json" }, + }) ); const result = await readChatSnapshot("v99-session"); @@ -154,10 +157,11 @@ describe("chat snapshot helpers", () => { it("returns undefined when `messages` field is missing or wrong type", async () => { stubApiClient({}); - stubFetch(async () => - new Response(JSON.stringify({ version: 1, savedAt: 1, messages: "not-an-array" }), { - status: 200, - }) + stubFetch( + async () => + new Response(JSON.stringify({ version: 1, savedAt: 1, messages: "not-an-array" }), { + status: 200, + }) ); const result = await readChatSnapshot("bad-shape-session"); @@ -190,9 +194,7 @@ describe("chat snapshot helpers", () => { it("returns undefined when the response is not an object", async () => { stubApiClient({}); - stubFetch(async () => - new Response(JSON.stringify("just-a-string"), { status: 200 }) - ); + stubFetch(async () => new Response(JSON.stringify("just-a-string"), { status: 200 })); const result = await readChatSnapshot("string-response"); expect(result).toBeUndefined(); @@ -224,7 +226,9 @@ describe("chat snapshot helpers", () => { stubApiClient({}); stubFetch(async () => new Response("forbidden", { status: 403 })); - await expect(writeChatSnapshot("forbidden-session", buildSnapshot())).resolves.toBeUndefined(); + await expect( + writeChatSnapshot("forbidden-session", buildSnapshot()) + ).resolves.toBeUndefined(); }); it("returns without throwing on a fetch network error (warns)", async () => { diff --git a/packages/trigger-sdk/test/chatHandover.test.ts b/packages/trigger-sdk/test/chatHandover.test.ts index 72b3fa7e44..a101b91494 100644 --- a/packages/trigger-sdk/test/chatHandover.test.ts +++ b/packages/trigger-sdk/test/chatHandover.test.ts @@ -98,8 +98,12 @@ describe("chat.handover", () => { const agent = chat.agent({ id: "chat.handover.pure-text", - onChatStart: () => { order.push("onChatStart"); }, - onTurnStart: () => { order.push("onTurnStart"); }, + onChatStart: () => { + order.push("onChatStart"); + }, + onTurnStart: () => { + order.push("onTurnStart"); + }, onTurnComplete: ({ responseMessage }) => { order.push("onTurnComplete"); capturedResponse = { @@ -265,9 +269,7 @@ describe("chat.handover", () => { // synthesized partial must carry it (with provider metadata, so an // Anthropic signature survives a UIMessage -> ModelMessage round // trip) or the durable history loses the step-1 thinking. - let captured: - | { partTypes?: string[]; reasoningText?: string; meta?: unknown } - | undefined; + let captured: { partTypes?: string[]; reasoningText?: string; meta?: unknown } | undefined; const agent = chat.agent({ id: "chat.handover.reasoning", @@ -279,9 +281,9 @@ describe("chat.handover", () => { .filter((p) => p.type === "reasoning") .map((p) => (p as { text?: string }).text || "") .join(""), - meta: (parts.find((p) => p.type === "reasoning") as - | { providerMetadata?: unknown } - | undefined)?.providerMetadata, + meta: ( + parts.find((p) => p.type === "reasoning") as { providerMetadata?: unknown } | undefined + )?.providerMetadata, }; }, run: async ({ messages, signal }) => { @@ -338,9 +340,7 @@ describe("chat.handover", () => { const runFn = vi.fn(); const stored: { id: string; role: string; parts: unknown[] }[] = []; const hydrateIncomingRoles: string[] = []; - let captured: - | { responseId?: string; responseText?: string; roles?: string[] } - | undefined; + let captured: { responseId?: string; responseText?: string; roles?: string[] } | undefined; const agent = chat.agent({ id: "chat.handover.hydrate-pure-text", diff --git a/packages/trigger-sdk/test/chatHandoverBackends.test.ts b/packages/trigger-sdk/test/chatHandoverBackends.test.ts index 29b45172e9..6658358667 100644 --- a/packages/trigger-sdk/test/chatHandoverBackends.test.ts +++ b/packages/trigger-sdk/test/chatHandoverBackends.test.ts @@ -57,13 +57,21 @@ const TOOL_CALL_PARTIAL: ModelMessage[] = [ content: [ { type: "text", text: "let me check the weather" }, { type: "tool-call", toolCallId: "tc-1", toolName: "weather", input: { city: "tokyo" } }, - { type: "tool-approval-request", approvalId: "handover-approval-1", toolCallId: "tc-1" } as never, + { + type: "tool-approval-request", + approvalId: "handover-approval-1", + toolCallId: "tc-1", + } as never, ], }, { role: "tool", content: [ - { type: "tool-approval-response", approvalId: "handover-approval-1", approved: true } as never, + { + type: "tool-approval-response", + approvalId: "handover-approval-1", + approved: true, + } as never, ], }, ]; @@ -293,7 +301,11 @@ describe("chat.customAgent + headStart handover", () => { const prior: UIMessage[] = [ { id: "u-1", role: "user", parts: [{ type: "text", text: "hello" }] }, // Already-persisted partial under the same id the handover uses. - { id: "asst-dup", role: "assistant", parts: [{ type: "text", text: "Hi there, hope you're well." }] }, + { + id: "asst-dup", + role: "assistant", + parts: [{ type: "text", text: "Hi there, hope you're well." }], + }, ]; const agent = chat.customAgent({ diff --git a/packages/trigger-sdk/test/merge-by-id.test.ts b/packages/trigger-sdk/test/merge-by-id.test.ts index 1c0091273c..7de51e2351 100644 --- a/packages/trigger-sdk/test/merge-by-id.test.ts +++ b/packages/trigger-sdk/test/merge-by-id.test.ts @@ -62,10 +62,7 @@ describe("mergeByIdReplaceWins", () => { }); it("replaces by id when `b` has a colliding entry β€” replay wins", () => { - const a = [ - userMessage("u-1", "hi"), - assistantMessage("a-1", "stale-version"), - ]; + const a = [userMessage("u-1", "hi"), assistantMessage("a-1", "stale-version")]; const b = [assistantMessage("a-1", "fresh-version")]; const result = mergeByIdReplaceWins(a, b); expect(result).toHaveLength(2); @@ -80,10 +77,7 @@ describe("mergeByIdReplaceWins", () => { userMessage("u-2", "second"), assistantMessage("a-2", "also-stale"), ]; - const b = [ - assistantMessage("a-1", "fresh-1"), - assistantMessage("a-2", "fresh-2"), - ]; + const b = [assistantMessage("a-1", "fresh-1"), assistantMessage("a-2", "fresh-2")]; const result = mergeByIdReplaceWins(a, b); expect(result.map((m) => m.id)).toEqual(["u-1", "a-1", "u-2", "a-2"]); expect((result[1]!.parts[0] as { text: string }).text).toBe("fresh-1"); @@ -105,10 +99,18 @@ describe("mergeByIdReplaceWins", () => { const a = [ userMessage("u-1", "first"), // Synthetic message missing the id field β€” should append, never replace. - { id: "" as string, role: "assistant", parts: [{ type: "text", text: "no-id-a" }] } as UIMessage, + { + id: "" as string, + role: "assistant", + parts: [{ type: "text", text: "no-id-a" }], + } as UIMessage, ]; const b = [ - { id: "" as string, role: "assistant", parts: [{ type: "text", text: "no-id-b" }] } as UIMessage, + { + id: "" as string, + role: "assistant", + parts: [{ type: "text", text: "no-id-b" }], + } as UIMessage, ]; const result = mergeByIdReplaceWins(a, b); expect(result).toHaveLength(3); diff --git a/packages/trigger-sdk/test/mockChatAgent.test.ts b/packages/trigger-sdk/test/mockChatAgent.test.ts index c2001de723..222d5d2ba0 100644 --- a/packages/trigger-sdk/test/mockChatAgent.test.ts +++ b/packages/trigger-sdk/test/mockChatAgent.test.ts @@ -205,9 +205,7 @@ describe("mockChatAgent", () => { // `upsertIncomingMessage` does. if (trigger === "submit-message" && incomingMessages.length > 0) { const newMsg = incomingMessages[incomingMessages.length - 1]!; - const exists = newMsg.id - ? stored.some((m) => m.id === newMsg.id) - : false; + const exists = newMsg.id ? stored.some((m) => m.id === newMsg.id) : false; if (!exists) stored.push(newMsg); } return [...stored]; @@ -308,7 +306,12 @@ describe("mockChatAgent", () => { }); }, run: async ({ messages, signal }) => { - return streamText({ model, messages, tools: { askUser: askUserTool }, abortSignal: signal }); + return streamText({ + model, + messages, + tools: { askUser: askUserTool }, + abortSignal: signal, + }); }, }); @@ -404,9 +407,7 @@ describe("mockChatAgent", () => { hydrateMessages: async () => [dbAssistant as any], onTurnComplete: async ({ uiMessages }) => { const head = uiMessages.find((m: any) => m.id === HEAD_ID); - mergedToolPart = (head?.parts ?? []).find( - (p: any) => p?.toolCallId === TC - ); + mergedToolPart = (head?.parts ?? []).find((p: any) => p?.toolCallId === TC); }, run: async ({ messages, signal }) => { return streamText({ @@ -482,9 +483,7 @@ describe("mockChatAgent", () => { hydrateMessages: async () => [dbAssistant as any], onTurnComplete: async ({ uiMessages }) => { const head = uiMessages.find((m: any) => m.id === HEAD_ID); - mergedToolPart = (head?.parts ?? []).find( - (p: any) => p?.toolCallId === TC - ); + mergedToolPart = (head?.parts ?? []).find((p: any) => p?.toolCallId === TC); }, run: async ({ messages, signal }) => { return streamText({ @@ -560,9 +559,7 @@ describe("mockChatAgent", () => { hydrateMessages: async () => [dbAssistant as any], onTurnComplete: async ({ uiMessages }) => { const head = uiMessages.find((m: any) => m.id === HEAD_ID); - mergedToolPart = (head?.parts ?? []).find( - (p: any) => p?.toolCallId === TC - ); + mergedToolPart = (head?.parts ?? []).find((p: any) => p?.toolCallId === TC); }, run: async ({ messages, signal }) => { return streamText({ @@ -675,9 +672,7 @@ describe("mockChatAgent", () => { }); // Recommended pattern: validate only user messages, since HITL // continuations carry slim assistants the AI SDK schema rejects. - const userMessages = messages.filter( - (m: any) => m.role === "user" - ); + const userMessages = messages.filter((m: any) => m.role === "user"); if (userMessages.length > 0) { await validateUIMessages({ messages: userMessages, @@ -824,9 +819,7 @@ describe("mockChatAgent", () => { await harness.sendMessage(userMessage("hi")); await new Promise((r) => setTimeout(r, 50)); - const turn1Assistant = turnsSeen.at(-1)?.uiMessages.find( - (m: any) => m.role === "assistant" - ); + const turn1Assistant = turnsSeen.at(-1)?.uiMessages.find((m: any) => m.role === "assistant"); expect(turn1Assistant).toBeTruthy(); const HEAD_ID = turn1Assistant!.id; @@ -848,9 +841,7 @@ describe("mockChatAgent", () => { const turn2 = turnsSeen.at(-1); const head = turn2!.uiMessages.find((m: any) => m.id === HEAD_ID); - const toolPart = (head?.parts ?? []).find( - (p: any) => p?.toolCallId === TC - ); + const toolPart = (head?.parts ?? []).find((p: any) => p?.toolCallId === TC); expect(toolPart?.state).toBe("output-available"); expect(toolPart?.output).toEqual({ color: "blue" }); // Snapshot's `input` survived the merge. @@ -1038,9 +1029,7 @@ describe("mockChatAgent", () => { // No additional model call; console.warn fired with our marker text. expect(runSpy.mock.calls.length).toBe(baselineRun); expect( - warnSpy.mock.calls.some((args) => - (args[0] as string).includes("no `onAction` handler") - ) + warnSpy.mock.calls.some((args) => (args[0] as string).includes("no `onAction` handler")) ).toBe(true); const sawTurnComplete = actionTurn.rawChunks.some( @@ -2061,8 +2050,7 @@ describe("mockChatAgent", () => { id: "onChatStart-gate.fresh-baseline", onChatStart, onTurnStart, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "fresh-baseline" }); try { @@ -2089,8 +2077,7 @@ describe("mockChatAgent", () => { hydrateMessages: async ({ incomingMessages }) => incomingMessages, onChatStart, onTurnStart, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "continuation-skip", @@ -2124,8 +2111,7 @@ describe("mockChatAgent", () => { hydrateMessages: async ({ incomingMessages }) => incomingMessages, onChatStart, onTurnStart, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "oom-retry-skip", diff --git a/packages/trigger-sdk/test/promptCaching.test.ts b/packages/trigger-sdk/test/promptCaching.test.ts index b20bf01285..6834596b7c 100644 --- a/packages/trigger-sdk/test/promptCaching.test.ts +++ b/packages/trigger-sdk/test/promptCaching.test.ts @@ -126,7 +126,9 @@ describe("chat prompt caching β€” system providerOptions", () => { messages, abortSignal: signal, ...chat.toStreamTextOptions({ - systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } } }, + systemProviderOptions: { + anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } }, + }, }), }), }); @@ -186,7 +188,9 @@ describe("chat prompt caching β€” system providerOptions", () => { messages, abortSignal: signal, ...chat.toStreamTextOptions({ - systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } } }, + systemProviderOptions: { + anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } }, + }, }), }), }); diff --git a/packages/trigger-sdk/test/recovery-boot.test.ts b/packages/trigger-sdk/test/recovery-boot.test.ts index 5d7b4cd221..0b11567fb9 100644 --- a/packages/trigger-sdk/test/recovery-boot.test.ts +++ b/packages/trigger-sdk/test/recovery-boot.test.ts @@ -71,8 +71,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { const agent = chat.agent({ id: "recovery-boot.no-state", onRecoveryBoot, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "no-state", @@ -102,8 +101,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { captured.event = event as never; return {}; }, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "partial-fires-hook", @@ -162,8 +160,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { captured.event = event; return {}; }, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "pending-tool-from-raw", @@ -173,12 +170,13 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { harness.seedSessionInTail([u1 as never]); // Install AFTER mockChatAgent β€” its constructor sets its own default // override that we want to replace for this test. - __setReplaySessionOutTailImplForTests(async () => - ({ - settled: [], - partial: cleanedPartial, - partialRaw: rawPartial, - }) as never + __setReplaySessionOutTailImplForTests( + async () => + ({ + settled: [], + partial: cleanedPartial, + partialRaw: rawPartial, + }) as never ); try { await new Promise((r) => setTimeout(r, 50)); @@ -207,8 +205,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { const agent = chat.agent({ id: "recovery-boot.inflight-users-no-partial", onRecoveryBoot, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "inflight-users-no-partial", @@ -237,8 +234,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { const agent = chat.agent({ id: "recovery-boot.default-dispatch", // NO onRecoveryBoot β€” exercise the default path - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "default-dispatch", @@ -277,8 +273,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { })); } }, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "smart-default", @@ -292,11 +287,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { // Turn 1 fires with the follow-up user (u2). Its chain should // include [u1 (original), a-partial, u2 (follow-up)]. expect(turnCount).toBe(1); - expect(observedChain.map((m) => m.role)).toEqual([ - "user", - "assistant", - "user", - ]); + expect(observedChain.map((m) => m.role)).toEqual(["user", "assistant", "user"]); expect(observedChain[0]!.idHead).toBe("u-1"); expect(observedChain[1]!.idHead).toBe("a-partial"); expect(observedChain[2]!.idHead).toBe("u-2"); @@ -318,8 +309,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { const agent = chat.agent({ id: "recovery-boot.suppress-dispatch", onRecoveryBoot: async (): Promise => ({ recoveredTurns: [] }), - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "suppress-dispatch", @@ -356,8 +346,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { onTurnStart: async ({ uiMessages }) => { observedMessageCount = uiMessages.length; }, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "chain-override", @@ -387,8 +376,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { id: "recovery-boot.hydrate-skips", hydrateMessages: async ({ incomingMessages }) => incomingMessages, onRecoveryBoot, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "hydrate-skips", @@ -423,8 +411,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { order.push("beforeBoot"); }, }), - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "before-boot", @@ -459,8 +446,7 @@ describe("onRecoveryBoot β€” chat.agent recovery hook", () => { onRecoveryBoot: async () => { throw new Error("kaboom"); }, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "hook-throws", diff --git a/packages/trigger-sdk/test/replay-session-in.test.ts b/packages/trigger-sdk/test/replay-session-in.test.ts index a90ecc1539..3d04974a9b 100644 --- a/packages/trigger-sdk/test/replay-session-in.test.ts +++ b/packages/trigger-sdk/test/replay-session-in.test.ts @@ -48,11 +48,21 @@ describe("replaySessionInTail", () => { stubReadRecords([ { kind: "message", - payload: { chatId: "c1", trigger: "submit-message", message: u1, metadata: { userId: "a" } }, + payload: { + chatId: "c1", + trigger: "submit-message", + message: u1, + metadata: { userId: "a" }, + }, }, { kind: "message", - payload: { chatId: "c1", trigger: "submit-message", message: u2, metadata: { userId: "b" } }, + payload: { + chatId: "c1", + trigger: "submit-message", + message: u2, + metadata: { userId: "b" }, + }, }, ]); diff --git a/packages/trigger-sdk/test/replay-session-out.test.ts b/packages/trigger-sdk/test/replay-session-out.test.ts index ed78ecec14..0111ec200b 100644 --- a/packages/trigger-sdk/test/replay-session-out.test.ts +++ b/packages/trigger-sdk/test/replay-session-out.test.ts @@ -179,7 +179,11 @@ describe("replaySessionOutTail", () => { // Trailing turn: starts a tool call but never resolves it. { type: "start", messageId: "a-2", messageMetadata: { role: "assistant" } } as UIMessageChunk, { type: "tool-input-start", toolCallId: "tc-cut", toolName: "search" } as UIMessageChunk, - { type: "tool-input-delta", toolCallId: "tc-cut", inputTextDelta: '{"q":"x"}' } as UIMessageChunk, + { + type: "tool-input-delta", + toolCallId: "tc-cut", + inputTextDelta: '{"q":"x"}', + } as UIMessageChunk, // No tool-input-end, no tool-call, no finish β†’ orphaned. ]); @@ -192,9 +196,9 @@ describe("replaySessionOutTail", () => { // would represent a tool the next turn would re-process. const trailing = result.find((m) => m.id === "a-2"); if (trailing) { - const orphanedToolPart = (trailing.parts as Array<{ type: string; toolCallId?: string; state?: string }>).find( - (p) => p.toolCallId === "tc-cut" && p.state === "input-streaming" - ); + const orphanedToolPart = ( + trailing.parts as Array<{ type: string; toolCallId?: string; state?: string }> + ).find((p) => p.toolCallId === "tc-cut" && p.state === "input-streaming"); expect(orphanedToolPart).toBeUndefined(); } }); @@ -205,7 +209,11 @@ describe("replaySessionOutTail", () => { // entirely (it never reached the next turn's accumulator). stubReadRecordsWithChunks([ ...textTurn("a-1", "complete"), - { type: "start", messageId: "a-orphan", messageMetadata: { role: "assistant" } } as UIMessageChunk, + { + type: "start", + messageId: "a-orphan", + messageMetadata: { role: "assistant" }, + } as UIMessageChunk, { type: "tool-input-start", toolCallId: "tc-orph", toolName: "search" } as UIMessageChunk, // No tool-input-end, no tool-call, no finish. ]); @@ -233,10 +241,7 @@ describe("replaySessionOutTail", () => { // The writer puts chunk objects directly into the record envelope; // the route forwards them as-is. A string body is malformed β€” the // consumer drops it defensively rather than JSON.parsing. - stubReadRecordsWithChunks([ - "not-an-object", - ...textTurn("a-1", "survived"), - ]); + stubReadRecordsWithChunks(["not-an-object", ...textTurn("a-1", "survived")]); const result = await replaySessionOutTail("garbage-session"); expect(result).toHaveLength(1); @@ -247,12 +252,7 @@ describe("replaySessionOutTail", () => { // The consumer requires `chunk` to be a non-null object with a // string `type` field. Records that arrive as primitives // (number, null, string) are dropped silently. - stubReadRecordsWithChunks([ - 42, - null, - "just-a-string", - ...textTurn("a-1", "survived"), - ]); + stubReadRecordsWithChunks([42, null, "just-a-string", ...textTurn("a-1", "survived")]); const result = await replaySessionOutTail("primitive-data-session"); expect(result).toHaveLength(1); @@ -260,11 +260,7 @@ describe("replaySessionOutTail", () => { }); it("ignores chunks missing a `type` field", async () => { - stubReadRecordsWithChunks([ - { foo: "bar" }, - { type: 42 }, - ...textTurn("a-1", "valid"), - ]); + stubReadRecordsWithChunks([{ foo: "bar" }, { type: 42 }, ...textTurn("a-1", "valid")]); const result = await replaySessionOutTail("typeless-session"); expect(result).toHaveLength(1); @@ -277,7 +273,11 @@ describe("replaySessionOutTail", () => { // a single corrupt segment shouldn't sink the entire replay. stubReadRecordsWithChunks([ // Malformed: text-end with no preceding text-start. - { type: "start", messageId: "bad-1", messageMetadata: { role: "assistant" } } as UIMessageChunk, + { + type: "start", + messageId: "bad-1", + messageMetadata: { role: "assistant" }, + } as UIMessageChunk, { type: "text-end", id: "no-such-text" } as UIMessageChunk, { type: "finish" } as UIMessageChunk, ...textTurn("a-1", "after-bad"), diff --git a/packages/trigger-sdk/test/wire-shape.test.ts b/packages/trigger-sdk/test/wire-shape.test.ts index 6214f3ca01..54a5074b0b 100644 --- a/packages/trigger-sdk/test/wire-shape.test.ts +++ b/packages/trigger-sdk/test/wire-shape.test.ts @@ -180,13 +180,17 @@ describe("upsertIncomingMessage", () => { const head = { id: "asst-1", role: "assistant" as const, - parts: [{ type: "tool-search", toolCallId: "tc-1", state: "input-available", input: {} } as never], + parts: [ + { type: "tool-search", toolCallId: "tc-1", state: "input-available", input: {} } as never, + ], }; const stored: UIMessage[] = [userMsg("u-1", "hi"), head]; const slim = { id: "asst-1", role: "assistant" as const, - parts: [{ type: "tool-search", toolCallId: "tc-1", state: "output-available", output: {} } as never], + parts: [ + { type: "tool-search", toolCallId: "tc-1", state: "output-available", output: {} } as never, + ], }; const mutated = upsertIncomingMessage(stored, { trigger: "submit-message", @@ -243,7 +247,10 @@ describe("upsertIncomingMessage", () => { it("pushes when newMsg has no id (no dedup possible)", () => { const stored: UIMessage[] = [userMsg("u-1", "hi")]; - const incoming = { role: "user", parts: [{ type: "text", text: "no id" }] } as unknown as UIMessage; + const incoming = { + role: "user", + parts: [{ type: "text", text: "no id" }], + } as unknown as UIMessage; const mutated = upsertIncomingMessage(stored, { trigger: "submit-message", incomingMessages: [incoming], @@ -455,7 +462,9 @@ describe("ChatTaskWirePayload (compile-time shape)", () => { // forces a compile error rather than letting the wire silently grow // back. type WirePayloadKeys = keyof ChatTaskWirePayload; - expectTypeOf().not.toEqualTypeOf<"messages" | Exclude>(); + expectTypeOf().not.toEqualTypeOf< + "messages" | Exclude + >(); // Also confirm the absence at the value level β€” a payload literal // with `messages` would be a TS error if uncommented: // @@ -478,18 +487,13 @@ describe("ChatTaskWirePayload (compile-time shape)", () => { it("requires `chatId: string` and `trigger: `", () => { expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf< - | "submit-message" - | "regenerate-message" - | "preload" - | "close" - | "action" - | "handover-prepare" + "submit-message" | "regenerate-message" | "preload" | "close" | "action" | "handover-prepare" >(); }); }); describe("ChatInputChunk envelope", () => { - it("wraps a wire payload in `kind: \"message\"` shape", () => { + it('wraps a wire payload in `kind: "message"` shape', () => { const userMsg: UIMessage = { id: "u-1", role: "user", @@ -511,7 +515,7 @@ describe("ChatInputChunk envelope", () => { } }); - it("supports `kind: \"stop\"` records (no payload)", () => { + it('supports `kind: "stop"` records (no payload)', () => { const chunk: ChatInputChunk = { kind: "stop", message: "user-canceled" }; const decoded = JSON.parse(JSON.stringify(chunk)) as ChatInputChunk; expect(decoded.kind).toBe("stop"); @@ -520,7 +524,7 @@ describe("ChatInputChunk envelope", () => { } }); - it("supports `kind: \"handover\"` records (with partialAssistantMessage)", () => { + it('supports `kind: "handover"` records (with partialAssistantMessage)', () => { const chunk: ChatInputChunk = { kind: "handover", partialAssistantMessage: [ @@ -533,7 +537,7 @@ describe("ChatInputChunk envelope", () => { expect(decoded.kind).toBe("handover"); }); - it("supports `kind: \"handover-skip\"` records", () => { + it('supports `kind: "handover-skip"` records', () => { const chunk: ChatInputChunk = { kind: "handover-skip" }; const decoded = JSON.parse(JSON.stringify(chunk)) as ChatInputChunk; expect(decoded.kind).toBe("handover-skip"); diff --git a/packages/trigger-sdk/vitest.config.ts b/packages/trigger-sdk/vitest.config.ts index 8387992eaa..8db9c42ae0 100644 --- a/packages/trigger-sdk/vitest.config.ts +++ b/packages/trigger-sdk/vitest.config.ts @@ -6,4 +6,3 @@ export default defineConfig({ globals: true, }, }); - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 156a5ee7ac..7c3d6c1047 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,21 +118,18 @@ importers: autoprefixer: specifier: ^10.4.12 version: 10.4.13(postcss@8.5.10) - eslint-plugin-turbo: - specifier: ^2.0.4 - version: 2.0.5(eslint@8.31.0) - lefthook: - specifier: ^1.11.3 - version: 1.11.3 + oxfmt: + specifier: ^0.54.0 + version: 0.54.0 + oxlint: + specifier: ^1.69.0 + version: 1.70.0 pkg-pr-new: specifier: 0.0.75 version: 0.0.75 pkg-types: specifier: 1.1.3 version: 1.1.3 - prettier: - specifier: ^3.0.0 - version: 3.0.0 tsx: specifier: ^3.7.1 version: 3.12.2 @@ -921,9 +918,6 @@ importers: '@remix-run/dev': specifier: 2.17.4 version: 2.17.4(@remix-run/react@2.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4))(@remix-run/serve@2.17.4(typescript@5.5.4))(@types/node@20.14.14)(bufferutil@4.0.9)(jiti@2.6.1)(lightningcss@1.29.2)(terser@5.46.1)(tsx@4.20.6)(typescript@5.5.4)(vite@6.4.2(@types/node@20.14.14)(jiti@2.6.1)(lightningcss@1.29.2)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0))(yaml@2.9.0) - '@remix-run/eslint-config': - specifier: 2.17.4 - version: 2.17.4(eslint@8.31.0)(react@18.3.1)(typescript@5.5.4) '@remix-run/testing': specifier: ^2.17.4 version: 2.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4) @@ -954,9 +948,6 @@ importers: '@types/cookie': specifier: ^0.6.0 version: 0.6.0 - '@types/eslint': - specifier: ^8.4.6 - version: 8.4.10 '@types/express': specifier: ^4.17.13 version: 4.17.15 @@ -1017,12 +1008,6 @@ importers: '@types/ws': specifier: ^8.5.3 version: 8.5.4 - '@typescript-eslint/eslint-plugin': - specifier: ^5.59.6 - version: 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) - '@typescript-eslint/parser': - specifier: ^5.59.6 - version: 5.59.6(eslint@8.31.0)(typescript@5.5.4) autoevals: specifier: ^0.0.130 version: 0.0.130(encoding@0.1.13)(ws@8.12.0(bufferutil@4.0.9)) @@ -1041,21 +1026,6 @@ importers: esbuild: specifier: ^0.15.10 version: 0.15.18 - eslint: - specifier: ^8.24.0 - version: 8.31.0 - eslint-config-prettier: - specifier: ^8.5.0 - version: 8.6.0(eslint@8.31.0) - eslint-plugin-import: - specifier: ^2.29.1 - version: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - eslint-plugin-react-hooks: - specifier: ^4.6.2 - version: 4.6.2(eslint@8.31.0) - eslint-plugin-turbo: - specifier: ^2.0.4 - version: 2.0.5(eslint@8.31.0) 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) @@ -1068,12 +1038,6 @@ importers: postcss-loader: specifier: ^8.1.1 version: 8.1.1(postcss@8.5.10)(typescript@5.5.4)(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)) - prettier: - specifier: ^2.8.8 - version: 2.8.8 - prettier-plugin-tailwindcss: - specifier: ^0.3.0 - version: 0.3.0(prettier@2.8.8) prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -3095,13 +3059,6 @@ packages: resolution: {integrity: sha512-2EENLmhpwplDux5PSsZnSbnSkB3tZ6QTksgO25xwEL7pIDcNOMhF5v/s6RzwjMZzZzw9Ofc30gHv5ChCC8pifQ==} engines: {node: '>=6.9.0'} - '@babel/eslint-parser@7.21.8': - resolution: {integrity: sha512-HLhI+2q+BP3sf78mFUZNCGc10KEmoUqtUT1OCdMZsN+qr4qFeLUod62/zAnF3jNQstwyasDkZnVXwfK2Bml7MQ==} - engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} - peerDependencies: - '@babel/core': '>=7.11.0' - eslint: ^7.5.0 || ^8.0.0 - '@babel/generator@7.24.7': resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} engines: {node: '>=6.9.0'} @@ -3256,42 +3213,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-display-name@7.18.6': - resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-development@7.18.6': - resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx@7.22.15': - resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-pure-annotations@7.18.6': - resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.21.3': resolution: {integrity: sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/preset-react@7.18.6': - resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/preset-typescript@7.21.5': resolution: {integrity: sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA==} engines: {node: '>=6.9.0'} @@ -4944,20 +4871,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.5.1': - resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/eslintrc@1.4.1': - resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@fastify/accept-negotiator@2.0.1': resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} @@ -5131,19 +5044,6 @@ packages: peerDependencies: '@hono/node-server': ^1.11.1 - '@humanwhocodes/config-array@0.11.8': - resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/object-schema@1.2.1': - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - deprecated: Use @eslint/object-schema instead - '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -5323,9 +5223,6 @@ packages: '@cfworker/json-schema': optional: true - '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': - resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} - '@nodable/entities@2.1.0': resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} @@ -5890,6 +5787,250 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@oxfmt/binding-android-arm-eabi@0.54.0': + resolution: {integrity: sha512-NAtpl/SiaeU103e7/OmZw0MvUnsUUopW7hEm/ecegJg7YM0skQaA0IXEZoyTV6NUdiNPupdIUreRqUZTShbn/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.54.0': + resolution: {integrity: sha512-B4VZfBUlKK1rmMChsssNZbkZjE8+FzG3avMjGgMDwbGxXRoXkoeXiAZ+78Oa+eyDPHvDCiUb4zH/vmCOUSafLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.54.0': + resolution: {integrity: sha512-i02vF75b+ePsQP3tHqSxVYI5S6b8X/xqdPu7/mDHXtpgXLTYXi3jJmfHU0j+dnZZDKaYTx/ioCK7QYJmtiJR2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.54.0': + resolution: {integrity: sha512-8VMFvGvooXj7mswkbrhdVZ2/sgiDaBzWpkkbtO+qGDLV4EfJd67nQadHkQC0ZNbaWA9ajXfqI6i7PZLIeDzxEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.54.0': + resolution: {integrity: sha512-0cRHnp43WN1Jrc5s0BdbdKgR1XirdvHy7TAFi3JEsoEVQVJxTXMbpVd76sxXlgRswNMDhVFSJw+y7Eb8mEavFQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': + resolution: {integrity: sha512-JyQAk3hK/OEtup7Rw6kZwfdzbKqTVD5jXXb8Xpfay29suwZyfBDMVW/bj4RqEPySYWc6zCp198pOluf8n5uYzg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.54.0': + resolution: {integrity: sha512-qnvLatTpM8vtvjOfcckBOzJjk+n6ce/wwpP8OFeUrD5aNLYcKyWAitwj+Rk3PK9jGanbZvKsJnv14JGQ6XqFdw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.54.0': + resolution: {integrity: sha512-SMkhnCzIYZYDk9vw3W/80eeYKmrMpGF0Giuxt4HruFlCH7jEtnPeb3SdQKMfgYi/dgtaf+hZAb5XWPYnxqCQ3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.54.0': + resolution: {integrity: sha512-QrwJlBFFKnxOd95TAaszpMbZBLzMoYMpGaQTZF8oibacnF5rv8l12IhILhQRPmksWiBqg0YSe2Mnl7ayeJAHSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.54.0': + resolution: {integrity: sha512-WILatiol/TUHTlhod7R09+7Az/XlhKwmY1MHfLZNmewltPWNN/EwxP2rQSHahibZ/cB8gmckEBjBOByD+5bYsQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.54.0': + resolution: {integrity: sha512-f05YMG4BH4G8S4ME6UM6fi1MnJ9094mrnvO5Pa4SJlMfWlUM+1/ZWMEF4NnjM7shZAvbHsHRuVYpUo0PHC4P9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.54.0': + resolution: {integrity: sha512-UfL+2hj1ClNqcCRT9s8vBU4axDpjxgVxX96G+9DYAYjoc5b0u15CJtn2jgsi9iM+EbGNc5CW1HVRgwVu76UsSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.54.0': + resolution: {integrity: sha512-3/XZe931Hka+J6NjnaqJzYpsWWxDTuRdUdwSQHnOuJEgbC+SehIMFJS8hsEjV7LBhVSL2OCnRLvbVW8O97XIyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.54.0': + resolution: {integrity: sha512-Ik93RlObtu43GbxApafayFjwYE06L6Xr08cSwpBPYbDrLp2ReZx0Jm1DqwRyYRnukUJy+rK2WaEvUQOxdytU9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.54.0': + resolution: {integrity: sha512-yZcakmPlD86CNymknd7KfW+FH+qfbqJH+i0h69CYfV1+KMoVeM9UED+8+TDVoU4haxI0NxY7RPCvRLy3Sqd2Qg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.54.0': + resolution: {integrity: sha512-GiVBZNnEZnKu00f1jTg49nomv187d0GQX+O+ocykoLeiaALuEO+swoTehHn9TehTfi7V8H0i0e/yvUjCqnwk1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.54.0': + resolution: {integrity: sha512-J0SSB8Z1Fre2sxRolYcW6Rl1RQmKdQ2hnHyq4YJrfBRiXTObLw4DXnIVraM/UyqGqwOi7yTrQA4VT7DPxlHVKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.54.0': + resolution: {integrity: sha512-O61UDVj8zz6yXJjkHPf05VaMLOXmEF8P5kf/N0W7AQMmd6bcQogl+KJc7rMutKTL524oE9iH32JXZClBFmEQIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.54.0': + resolution: {integrity: sha512-1MDpqJPiFqxWtIHas8vkb1VZ7f7eKyTffAwmO8isxQYMaG1OFKsH666BWLeXQLO+IWNfiMssLD55hbR1lIPTqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.70.0': + resolution: {integrity: sha512-zFh0P4cswmRvw6nkyb89dr18rRanuaCPAsEXsFDoQY8WdaquI8Pt4NWFjaMJg6L23cy5NeN8J9cBnREbWzZhaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.70.0': + resolution: {integrity: sha512-qI8o4HZjeGiBrWv+pJv4lH0Yi2Gl/JSp/EumBUApezJprIKa5PS4nU0lQsQngtky8k+SplQIOjv6hwu0SSxeyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.70.0': + resolution: {integrity: sha512-8KjgVVHI5F9nVwHCRwwA78Ty7zNKP4Wd9OeN5PSv3iu/F/u1RVXoOCgLhWqust6HmwQG6xc8c+RCyaWENy24+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.70.0': + resolution: {integrity: sha512-WVydssv5PSUBXFJTdNBWlmGkbNmvPGaFt/2SUT/EZRB6bq6bEOHmMlbnupZD5jmlEvi9+mZJHi8TCw15lyfSfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.70.0': + resolution: {integrity: sha512-hJucmUf8OlinHNb1R7fI4Fw6WsAstOz7i8nmkWQfiHoZXtbufNm+MxiDTIMk1ggh2Ro4vLzgQ+bKvRY54MZoRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.70.0': + resolution: {integrity: sha512-1BnS7wbCYDSXwWzJJ+mc3NURoha6m6m6RT5c6vgAY3oz7C3OVXP+S0awo2mRq97arrJkVvO3qRQfyAHL+76xtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.70.0': + resolution: {integrity: sha512-yKy/UdbR55+M2yEcuiV5DCNC/gdQAjr/GioUy50QwBzSrKm8ueWADqyRLS9Xk+qjNeCYGg6A8FvUBds56ttfqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.70.0': + resolution: {integrity: sha512-0A5XJ4alvmqFUFP/4oYSyaO+qLto/HrKEWTSaegiVl+HOufFngK2BjYw9x4RbwBt/du5QG6l5q1zeWiJYYG5yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.70.0': + resolution: {integrity: sha512-JiylyurlB0CLSedNtx1gzv3FvfWPF1h/2Y3BJszPLNt5XQFlBsH5ke0Jle3iJb3uqu5m2e7A/DwzpuCAHdiU+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.70.0': + resolution: {integrity: sha512-J8VPG7I3/HmgaU4u8pNU2kFx2+0U+vPLS1dXFxXOaR/2TQ0f8AC7DRz0SRGRI1bfphnX2hVYTTtLuhL4nYKL+Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.70.0': + resolution: {integrity: sha512-N2+4lV2KLN+oXTIIIwmWDhwkrnvqf5oX7Hw0zPjk+RuIVgiBQSOlJWF7uQoFx2siEYX0ZQ5cfSbEAHm+J3t7Wg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.70.0': + resolution: {integrity: sha512-1e2L7cFCvx9QDzq6NPP+0tABKb5z6nWHyddWTNKprEsjO9xNrAtPowuCGpjNXxkTdsMiZ4jc8YQ5SstZd4XK6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.70.0': + resolution: {integrity: sha512-Kwu/l/8GcYibCWA9m9N5pRXMIKVSsL/YbgpLzYkqDhWTiqdRfnNJ/+nqIKRKQiFbHWsdlHEhzMwruJK+qcEruA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.70.0': + resolution: {integrity: sha512-tap04CsHYOl0nSAQJfPNIuBxqEPB2HnhQqwaOXLg1jnp2XfRo8Fa814dA4QC4zpvTWXCjAAaCY1W5LOORkEQuQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.70.0': + resolution: {integrity: sha512-hzJa/WgvtJpbBD9rgfy0qe+MjbxOXNUT0bfR1S6EQQzfTtBFA9xg5q8KSwRrQ2QfSS+TaP4j+4mVPQrfNc6UNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.70.0': + resolution: {integrity: sha512-xbsaNSNzVSnaJACCUYr1HQMyY/Q/Q1LkePmHG3UvZPvGCYGNxrsZp9OmtA6ick8xH47ltRRbRrPCM1YXYcyC+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.70.0': + resolution: {integrity: sha512-icAEsUI7JbW1TMRdEXV83mVAInhRVQYuuAlPpxdGwJ95chNdnCzjloRW8GglT0WvzOEZSio6fnYSk2DJ2Hv7LQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.70.0': + resolution: {integrity: sha512-FHMSWbVsPVs/f+Jcl04ws4JJ2wUnauyTzlpxWRG/lSO/8GpX08Fo2gQZqdA6CrRFI+zvkxl+N/KwJGWfUwYVZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.70.0': + resolution: {integrity: sha512-ptOlKwCz7n4AKs5VweMqG6DAg677FmKOK+vBkkL9DMNgFATIQ+upqUYBTOEwRQyRAx1ncGlPlXleV2hIcm3z4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -5897,10 +6038,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@pkgr/utils@2.3.1': - resolution: {integrity: sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.37.0': resolution: {integrity: sha512-181WBLk4SRUyH1Q96VZl7BP6HcK0b7lbdeKisn3N/vnjitk+9HbdlFz/L5fey05vxaAhldIDnzo8KUoy8S3mmQ==} engines: {node: '>=16'} @@ -7281,17 +7418,6 @@ packages: wrangler: optional: true - '@remix-run/eslint-config@2.17.4': - resolution: {integrity: sha512-Hhslms8Kl0fXHDS5UJWwyJ/1YQzJhRLjNZF+IfRLmCHI/zCvJP4dfy9yiT5BHnHb/m6MUz3l1L8EpPItg0dD5Q==} - engines: {node: '>=18.0.0'} - peerDependencies: - eslint: ^8.0.0 - react: 18.3.1 - typescript: 5.5.4 - peerDependenciesMeta: - typescript: - optional: true - '@remix-run/express@2.17.4': resolution: {integrity: sha512-4zZs0L7v2pvAq896zHRLNMhoOKIPXM9qnYdHLbz4mpZUMbNAgQacGazArIrUV3M4g0gRMY0dLrt5CqMNrlBeYg==} engines: {node: '>=18.0.0'} @@ -7515,9 +7641,6 @@ packages: cpu: [x64] os: [win32] - '@rushstack/eslint-patch@1.2.0': - resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} - '@s2-dev/streamstore@0.22.10': resolution: {integrity: sha512-dtm+oFHVE8szINwOUoNQdx9xpGSJOrcAEvsxspPFvomjYKGnmhIRmU4OX8o6kxcPoiK76S1tPeU0smjZdmOngA==} @@ -8504,10 +8627,6 @@ packages: '@testcontainers/redis@11.14.0': resolution: {integrity: sha512-WX005slz2JMQPw2avbSjf5awVjpmFhOs5xCxeGSYLcV5ia4W1edv/P6MdOw4dZnvDQDuN5LfqNoV/ut3XGb2pA==} - '@testing-library/dom@8.19.1': - resolution: {integrity: sha512-P6iIPyYQ+qH8CvGauAqanhVnjrnRe0IZFSYCeGkSRW9q3u8bdVn2NPI+lasFyVsEQn1J/IFmp5Aax41+dAP9wg==} - engines: {node: '>=12'} - '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -8526,9 +8645,6 @@ packages: '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} - '@types/aria-query@5.0.1': - resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} - '@types/aws-lambda@8.10.152': resolution: {integrity: sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==} @@ -8686,18 +8802,12 @@ packages: '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - '@types/eslint@8.4.10': - resolution: {integrity: sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==} - '@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==} - '@types/estree@1.0.0': - resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -8746,12 +8856,6 @@ packages: '@types/json-query@2.2.3': resolution: {integrity: sha512-ygE4p8lyKzTBo9LF2K/u6MHnxPxbHY6wGvwM7TdAKhbP3SvEf+Y9aeVWedDiP8SMIPowTl9R/6awQYjiUTHz2g==} - '@types/json-schema@7.0.11': - resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} - - '@types/json-schema@7.0.13': - resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -8975,64 +9079,6 @@ packages: '@types/ws@8.5.4': resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} - '@typescript-eslint/eslint-plugin@5.59.6': - resolution: {integrity: sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/parser@5.59.6': - resolution: {integrity: sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/scope-manager@5.59.6': - resolution: {integrity: sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@typescript-eslint/type-utils@5.59.6': - resolution: {integrity: sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@5.59.6': - resolution: {integrity: sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@typescript-eslint/typescript-estree@5.59.6': - resolution: {integrity: sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/utils@5.59.6': - resolution: {integrity: sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - - '@typescript-eslint/visitor-keys@5.59.6': - resolution: {integrity: sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@uiw/codemirror-extensions-basic-setup@4.19.5': resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==} peerDependencies: @@ -9391,10 +9437,6 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -9438,9 +9480,6 @@ packages: resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} engines: {node: '>=10'} - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - arktype@2.1.20: resolution: {integrity: sha512-IZCEEXaJ8g+Ijd59WtSYwtjnqXiwM8sWQ5EjGamcto7+HVN9eK0C4p0zDlCuAwWhpqr6fIBkxPuYDl4/Mcj/+Q==} @@ -9451,33 +9490,14 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} - engines: {node: '>= 0.4'} - array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} - engines: {node: '>= 0.4'} - array.prototype.flat@1.3.1: resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} engines: {node: '>= 0.4'} - array.prototype.flat@1.3.2: - resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.2: - resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} - engines: {node: '>= 0.4'} - - array.prototype.tosorted@1.1.1: - resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==} - arraybuffer.prototype.slice@1.0.3: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} @@ -9507,9 +9527,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-types-flow@0.0.7: - resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} - ast-v8-to-istanbul@1.0.2: resolution: {integrity: sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==} @@ -9567,16 +9584,9 @@ packages: aws4fetch@1.0.18: resolution: {integrity: sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ==} - axe-core@4.6.2: - resolution: {integrity: sha512-b1WlTV8+XKLj9gZy2DZXgQiyDp9xkkoe2a6U6UbYccScq2wgH/YwCeI2/Jq2mgo0HzQxqJOjWZBLeA/mqsk5Mg==} - engines: {node: '>=4'} - axios@1.16.1: resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} - axobject-query@3.2.1: - resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} - b4a@1.6.6: resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} @@ -10520,9 +10530,6 @@ packages: dagre-d3-es@7.0.14: resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} - damerau-levenshtein@1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - dashdash@1.14.1: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} engines: {node: '>=0.10'} @@ -10571,14 +10578,6 @@ packages: supports-color: optional: true - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -10606,15 +10605,6 @@ packages: supports-color: optional: true - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -10653,9 +10643,6 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deep-object-diff@1.1.9: resolution: {integrity: sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==} @@ -10685,10 +10672,6 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -10800,17 +10783,6 @@ packages: resolution: {integrity: sha512-FbVf3Z8fY/kALB9s+P9epCpWhfi/r0N2DgYYcYpsAUlaTxPjdsitsFobnltb+lyCgAIvf9C+4PSWlTnHlJMf1w==} engines: {node: '>= 8.0'} - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - - dom-accessibility-api@0.5.15: - resolution: {integrity: sha512-8o+oVqLQZoruQPYy3uAAQtc6YbtSiRq5aPJBhJ82YTJRHvI6ofhYAkC81WmjFTnfUbqg6T3aCglIpU9p/5e7Cw==} - dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -10834,10 +10806,6 @@ packages: resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} engines: {node: '>=20'} - dotenv@16.0.3: - resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} - engines: {node: '>=12'} - dotenv@16.4.4: resolution: {integrity: sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==} engines: {node: '>=12'} @@ -11318,181 +11286,19 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-prettier@8.6.0: - resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-import-resolver-node@0.3.7: - resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} - - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-import-resolver-typescript@3.5.5: - resolution: {integrity: sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - - eslint-module-utils@2.8.1: - resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-es@3.0.1: - resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=4.19.1' - - eslint-plugin-import@2.29.1: - resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - - eslint-plugin-jest-dom@4.0.3: - resolution: {integrity: sha512-9j+n8uj0+V0tmsoS7bYC7fLhQmIvjRqRYEcbDSi+TKPsTThLLXCyj5swMSSf/hTleeMktACnn+HFqXBr5gbcbA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6', yarn: '>=1'} - peerDependencies: - eslint: ^6.8.0 || ^7.0.0 || ^8.0.0 - - eslint-plugin-jest@26.9.0: - resolution: {integrity: sha512-TWJxWGp1J628gxh2KhaH1H1paEdgE2J61BBF1I59c6xWeL5+D1BzMxGDN/nXAfX+aSkR5u80K+XhskK6Gwq9ng==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - jest: '*' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - jest: - optional: true - - eslint-plugin-jsx-a11y@6.7.1: - resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - - eslint-plugin-node@11.1.0: - resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=5.16.0' - - eslint-plugin-react-hooks@4.6.2: - resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - - eslint-plugin-react@7.32.2: - resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - - eslint-plugin-testing-library@5.11.0: - resolution: {integrity: sha512-ELY7Gefo+61OfXKlQeXNIDVVLPcvKTeiQOoMZG9TeuWa7Ln4dUNRv8JdRWBQI9Mbb427XGlVB1aa1QPZxBJM8Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} - peerDependencies: - eslint: ^7.5.0 || ^8.0.0 - - eslint-plugin-turbo@2.0.5: - resolution: {integrity: sha512-nCTXZdaKmdRybBdjnMrDFG+ppLc9toUqB01Hf0pfhkQw8OoC29oJIVPsCSvuL/W58RKD02CNEUrwnVt57t36IQ==} - peerDependencies: - eslint: '>6.6.0' - eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} - eslint-scope@7.1.1: - resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-utils@2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - - eslint-utils@3.0.0: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - - eslint-visitor-keys@1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - - eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - - eslint-visitor-keys@3.3.0: - resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@3.4.2: - resolution: {integrity: sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint@8.31.0: - resolution: {integrity: sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - - espree@9.4.1: - resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - espree@9.6.0: - resolution: {integrity: sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - esquery@1.4.0: - resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} - engines: {node: '>=0.10'} - esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -11533,10 +11339,6 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -11701,9 +11503,6 @@ packages: fast-json-stringify@6.0.1: resolution: {integrity: sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==} - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-npm-meta@0.2.2: resolution: {integrity: sha512-E+fdxeaOQGo/CMWc9f4uHFfgUPJRAu7N3uB8GBvB3SDPAIWJK4GKyYhkAGFq+GYrcbKNfQIz5VVQyJnDuPPCrg==} @@ -11797,10 +11596,6 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - file-type@19.6.0: resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==} engines: {node: '>=18'} @@ -11839,13 +11634,6 @@ packages: find-yarn-workspace-root2@1.2.16: resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} - flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} @@ -12090,10 +11878,6 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} - globals@13.19.0: - resolution: {integrity: sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==} - engines: {node: '>=8'} - globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} @@ -12102,9 +11886,6 @@ packages: resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} engines: {node: '>= 0.4'} - globalyzer@0.1.0: - resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -12535,11 +12316,6 @@ packages: is-deflate@1.0.0: resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12600,10 +12376,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-plain-obj@1.1.0: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} @@ -12683,10 +12455,6 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -12785,9 +12553,6 @@ packages: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} - js-sdsl@4.2.0: - resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} - js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -12850,9 +12615,6 @@ packages: json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-stable-stringify@1.3.0: resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} engines: {node: '>= 0.4'} @@ -12902,10 +12664,6 @@ packages: resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} engines: {node: '>=0.6.0'} - jsx-ast-utils@3.3.3: - resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} - engines: {node: '>=4.0'} - junk@4.0.1: resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} engines: {node: '>=12.20'} @@ -12948,12 +12706,6 @@ packages: resolution: {integrity: sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==} engines: {node: '>=20.10.0', npm: '>=10.2.3'} - language-subtag-registry@0.3.22: - resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} - - language-tags@1.0.5: - resolution: {integrity: sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==} - layerr@2.0.1: resolution: {integrity: sha512-z0730CwG/JO24evdORnyDkwG1Q7b7mF2Tp1qRQ0YvrMMARbt1DFG694SOv439Gm7hYKolyZyaB49YIrYIfZBdg==} @@ -12970,64 +12722,6 @@ packages: leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} - lefthook-darwin-arm64@1.11.3: - resolution: {integrity: sha512-IYzAOf8Qwqk7q+LoRyy7kSk9vzpUZ5wb/vLzEAH/F86Vay9AUaWe1f2pzeLwFg18qEc1QNklT69h9p/uLQMojA==} - cpu: [arm64] - os: [darwin] - - lefthook-darwin-x64@1.11.3: - resolution: {integrity: sha512-z/Wp7UMjE1Vyl+x9sjN3NvN6qKdwgHl+cDf98MKKDg/WyPE5XnzqLm9rLLJgImjyClfH7ptTfZxEyhTG3M3XvQ==} - cpu: [x64] - os: [darwin] - - lefthook-freebsd-arm64@1.11.3: - resolution: {integrity: sha512-QevwQ7lrv5wBCkk7LLTzT5KR3Bk/5nttSxT1UH2o0EsgirS/c2K5xSgQmV6m3CiZYuCe2Pja4BSIwN3zt17SMw==} - cpu: [arm64] - os: [freebsd] - - lefthook-freebsd-x64@1.11.3: - resolution: {integrity: sha512-PYbcyNgdJJ4J2pEO9Ss4oYo5yq4vmQGTKm3RTYbRx4viSWR65hvKCP0C4LnIqspMvmR05SJi2bqe7UBP2t60EA==} - cpu: [x64] - os: [freebsd] - - lefthook-linux-arm64@1.11.3: - resolution: {integrity: sha512-0pBMBAoafOAEg345eOPozsmRjWR0zCr6k+m5ZxwRBZbZx1bQFDqBakQ3TpFCphhcykmgFyaa1KeZJZUOrEsezA==} - cpu: [arm64] - os: [linux] - - lefthook-linux-x64@1.11.3: - resolution: {integrity: sha512-eiezheZ/bisBCMB2Ur0mctug/RDFyu39B5wzoE8y4z0W1yw6jHGrWMJ4Y8+5qKZ7fmdZg+7YPuMHZ2eFxOnhQA==} - cpu: [x64] - os: [linux] - - lefthook-openbsd-arm64@1.11.3: - resolution: {integrity: sha512-DRLTzXdtCj/TizpLcGSqXcnrqvgxeXgn/6nqzclIGqNdKCsNXDzpI0D3sP13Vwwmyoqv2etoTak2IHqZiXZDqg==} - cpu: [arm64] - os: [openbsd] - - lefthook-openbsd-x64@1.11.3: - resolution: {integrity: sha512-l7om+ZjWpYrVZyDuElwnucZhEqa7YfwlRaKBenkBxEh2zMje8O6Zodeuma1KmyDbSFvnvEjARo/Ejiot4gLXEw==} - cpu: [x64] - os: [openbsd] - - lefthook-windows-arm64@1.11.3: - resolution: {integrity: sha512-X0iTrql2gfPAkU2dzRwuHWgW5RcqCPbzJtKQ41X6Y/F7iQacRknmuYUGyC81funSvzGAsvlusMVLUvaFjIKnbA==} - cpu: [arm64] - os: [win32] - - lefthook-windows-x64@1.11.3: - resolution: {integrity: sha512-F+ORMn6YJXoS0EXU5LtN1FgV4QX9rC9LucZEkRmK6sKmS7hcb9IHpyb7siRGytArYzJvXVjPbxPBNSBdN4egZQ==} - cpu: [x64] - os: [win32] - - lefthook@1.11.3: - resolution: {integrity: sha512-HJp37y62j3j8qzAOODWuUJl4ysLwsDvCTBV6odr3jIRHR/a5e+tI14VQGIBcpK9ysqC3pGWyW5Rp9Jv1YDubyw==} - hasBin: true - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - light-my-request@6.6.0: resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} @@ -13270,10 +12964,6 @@ packages: resolution: {integrity: sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==} engines: {node: '>=12'} - lz-string@1.4.4: - resolution: {integrity: sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==} - hasBin: true - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -13911,12 +13601,6 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - nearley@2.20.1: resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} hasBin: true @@ -14122,25 +13806,6 @@ packages: resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} engines: {node: '>= 0.4'} - object.entries@1.1.6: - resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - - object.hasown@1.1.2: - resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==} - - object.values@1.2.0: - resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} - engines: {node: '>= 0.4'} - obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} @@ -14202,10 +13867,6 @@ packages: resolution: {integrity: sha512-dtbI5oW7987hwC9qjJTyABldTaa19SuyJse1QboWv3b0qCcrrLNVDqBx1XgELAjh9QTVQaP/C5b1nhQebd1H2A==} engines: {node: '>=18'} - open@8.4.0: - resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} - engines: {node: '>=12'} - openai@4.33.1: resolution: {integrity: sha512-0DH572aSxGTT1JPOXgJQ9mjiuSPg/7scPot8hLc5I1mfQxPxLXTZWJpWerKaIWOuPkR2nrB0SamGDEehH8RuWA==} hasBin: true @@ -14228,10 +13889,6 @@ packages: openid-client@6.3.3: resolution: {integrity: sha512-lTK8AV8SjqCM4qznLX0asVESAwzV39XTVdfMAM185ekuaZCnkWdPzcxMTXNlsm9tsUAMa1Q30MBmKAykdT1LWw==} - optionator@0.9.1: - resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} - engines: {node: '>= 0.8.0'} - ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -14246,6 +13903,32 @@ packages: outdent@0.8.0: resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} + oxfmt@0.54.0: + resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint@1.70.0: + resolution: {integrity: sha512-D6JgHtzkhRwvEC+A0Nw5AEc5bk8x5i1pHzvZIEf/a0C4hOzmAACNGtkDGPyFaxxX3ZVGxCPeig3P3rMM8XU3/g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.22.1' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + p-cancelable@1.1.0: resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} engines: {node: '>=6'} @@ -14858,85 +14541,20 @@ packages: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} engines: {node: '>=10'} - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - prepend-http@2.0.0: resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} engines: {node: '>=4'} - prettier-plugin-tailwindcss@0.3.0: - resolution: {integrity: sha512-009/Xqdy7UmkcTBpwlq7jsViDqXAYSOMLDrHAdTMlVZOrKfM2o9Ci7EMWTMZ7SkKBFTG04UM9F9iM2+4i6boDA==} - engines: {node: '>=12.17.0'} - peerDependencies: - '@ianvs/prettier-plugin-sort-imports': '*' - '@prettier/plugin-pug': '*' - '@shopify/prettier-plugin-liquid': '*' - '@shufo/prettier-plugin-blade': '*' - '@trivago/prettier-plugin-sort-imports': '*' - prettier: '>=2.2.0' - prettier-plugin-astro: '*' - prettier-plugin-css-order: '*' - prettier-plugin-import-sort: '*' - prettier-plugin-jsdoc: '*' - prettier-plugin-marko: '*' - prettier-plugin-organize-attributes: '*' - prettier-plugin-organize-imports: '*' - prettier-plugin-style-order: '*' - prettier-plugin-svelte: '*' - prettier-plugin-twig-melody: '*' - peerDependenciesMeta: - '@ianvs/prettier-plugin-sort-imports': - optional: true - '@prettier/plugin-pug': - optional: true - '@shopify/prettier-plugin-liquid': - optional: true - '@shufo/prettier-plugin-blade': - optional: true - '@trivago/prettier-plugin-sort-imports': - optional: true - prettier-plugin-astro: - optional: true - prettier-plugin-css-order: - optional: true - prettier-plugin-import-sort: - optional: true - prettier-plugin-jsdoc: - optional: true - prettier-plugin-marko: - optional: true - prettier-plugin-organize-attributes: - optional: true - prettier-plugin-organize-imports: - optional: true - prettier-plugin-style-order: - optional: true - prettier-plugin-svelte: - optional: true - prettier-plugin-twig-melody: - optional: true - prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true - prettier@3.0.0: - resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==} - engines: {node: '>=14'} - hasBin: true - prettier@3.8.3: resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} hasBin: true - pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - pretty-hrtime@1.0.3: resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} engines: {node: '>= 0.8'} @@ -15198,9 +14816,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -15422,10 +15037,6 @@ packages: resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} engines: {node: '>= 0.4'} - regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - registry-auth-token@4.2.2: resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} engines: {node: '>=6.0.0'} @@ -15580,10 +15191,6 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - requireindex@1.2.0: - resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} - engines: {node: '>=0.10.5'} - resend@3.2.0: resolution: {integrity: sha512-lDHhexiFYPoLXy7zRlJ8D5eKxoXy6Tr9/elN3+Vv7PkUoYuSSD1fpiIfa/JYXEWyiyN2UczkCTLpkT8dDPJ4Pg==} engines: {node: '>=18'} @@ -15614,10 +15221,6 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true - resolve@2.0.0-next.4: - resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} - hasBin: true - responselike@1.0.2: resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} @@ -15651,11 +15254,6 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rimraf@6.0.1: resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} engines: {node: 20 || >=22} @@ -16121,9 +15719,6 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string.prototype.matchall@4.0.8: - resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} - string.prototype.padend@3.1.4: resolution: {integrity: sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==} engines: {node: '>= 0.4'} @@ -16186,10 +15781,6 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} @@ -16291,10 +15882,6 @@ packages: engines: {node: 20 || >=22} hasBin: true - synckit@0.8.5: - resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} - engines: {node: ^14.18.0 || >=16.0.0} - systeminformation@5.31.7: resolution: {integrity: sha512-/8NC53e5nP9nmhn42/ncdOkyJnOoue/Vy+tJOyUGd1Yv66G069wK4rrziwhrqDETgk78CudTQupw5z19S5uoZw==} engines: {node: '>=8.0.0'} @@ -16345,10 +15932,6 @@ packages: tailwindcss@4.3.0: resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} - tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} @@ -16417,9 +16000,6 @@ packages: text-decoder@1.2.0: resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -16445,9 +16025,6 @@ packages: tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} - tiny-glob@0.2.9: - resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} - tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} @@ -16489,6 +16066,10 @@ packages: 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} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} @@ -16628,9 +16209,6 @@ packages: tsconfig-paths@3.14.1: resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -16674,12 +16252,6 @@ packages: typescript: optional: true - tsutils@3.21.0: - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: 5.5.4 - tsx@3.12.2: resolution: {integrity: sha512-ykAEkoBg30RXxeOMVeZwar+JH632dZn9EUJVyJwhfag62k6UO/dIyJEV58YuLF6e5BTdV/qmbQrpkWqjq9cUnQ==} hasBin: true @@ -16752,18 +16324,10 @@ packages: tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - type-fest@0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -17356,10 +16920,6 @@ packages: engines: {node: '>=8'} hasBin: true - word-wrap@1.2.3: - resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} - engines: {node: '>=0.10.0'} - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -19671,14 +19231,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/eslint-parser@7.21.8(@babel/core@7.22.17)(eslint@8.31.0)': - dependencies: - '@babel/core': 7.22.17 - '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.31.0 - eslint-visitor-keys: 2.1.0 - semver: 6.3.1 - '@babel/generator@7.24.7': dependencies: '@babel/types': 7.27.3 @@ -19841,31 +19393,6 @@ snapshots: '@babel/helper-plugin-utils': 7.24.0 '@babel/helper-simple-access': 7.22.5 - '@babel/plugin-transform-react-display-name@7.18.6(@babel/core@7.22.17)': - dependencies: - '@babel/core': 7.22.17 - '@babel/helper-plugin-utils': 7.24.0 - - '@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.22.17)': - dependencies: - '@babel/core': 7.22.17 - '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.22.17) - - '@babel/plugin-transform-react-jsx@7.22.15(@babel/core@7.22.17)': - dependencies: - '@babel/core': 7.22.17 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.17) - '@babel/types': 7.27.3 - - '@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.22.17)': - dependencies: - '@babel/core': 7.22.17 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-transform-typescript@7.21.3(@babel/core@7.22.17)': dependencies: '@babel/core': 7.22.17 @@ -19874,16 +19401,6 @@ snapshots: '@babel/helper-plugin-utils': 7.24.0 '@babel/plugin-syntax-typescript': 7.21.4(@babel/core@7.22.17) - '@babel/preset-react@7.18.6(@babel/core@7.22.17)': - dependencies: - '@babel/core': 7.22.17 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-validator-option': 7.22.15 - '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.22.17) - '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.22.17) - '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.22.17) - '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.22.17) - '@babel/preset-typescript@7.21.5(@babel/core@7.22.17)': dependencies: '@babel/core': 7.22.17 @@ -21041,27 +20558,6 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.31.0)': - dependencies: - eslint: 8.31.0 - eslint-visitor-keys: 3.4.2 - - '@eslint-community/regexpp@4.5.1': {} - - '@eslint/eslintrc@1.4.1': - dependencies: - ajv: 6.12.6 - debug: 4.4.3(supports-color@10.0.0) - espree: 9.6.0 - globals: 13.19.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - '@fastify/accept-negotiator@2.0.1': {} '@fastify/ajv-compiler@4.0.5': @@ -21277,18 +20773,6 @@ snapshots: - bufferutil - utf-8-validate - '@humanwhocodes/config-array@0.11.8': - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.4.3(supports-color@10.0.0) - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/object-schema@1.2.1': {} - '@iconify/types@2.0.0': {} '@iconify/utils@3.0.2': @@ -21607,10 +21091,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': - dependencies: - eslint-scope: 5.1.1 - '@nodable/entities@2.1.0': {} '@nodelib/fs.scandir@2.1.5': @@ -22375,20 +21855,125 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@oxfmt/binding-android-arm-eabi@0.54.0': + optional: true + + '@oxfmt/binding-android-arm64@0.54.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.54.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.54.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.54.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.54.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.54.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.54.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.54.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.54.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.54.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.70.0': + optional: true + + '@oxlint/binding-android-arm64@1.70.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.70.0': + optional: true + + '@oxlint/binding-darwin-x64@1.70.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.70.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.70.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.70.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.70.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.70.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.70.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.70.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.70.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.70.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.70.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.70.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.70.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.70.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.70.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.70.0': + optional: true + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': optional: true - '@pkgr/utils@2.3.1': - dependencies: - cross-spawn: 7.0.6 - is-glob: 4.0.3 - open: 8.4.0 - picocolors: 1.1.1 - tiny-glob: 0.2.9 - tslib: 2.8.1 - '@playwright/test@1.37.0': dependencies: '@types/node': 20.14.14 @@ -24314,33 +23899,6 @@ snapshots: - utf-8-validate - yaml - '@remix-run/eslint-config@2.17.4(eslint@8.31.0)(react@18.3.1)(typescript@5.5.4)': - dependencies: - '@babel/core': 7.22.17 - '@babel/eslint-parser': 7.21.8(@babel/core@7.22.17)(eslint@8.31.0) - '@babel/preset-react': 7.18.6(@babel/core@7.22.17) - '@rushstack/eslint-patch': 1.2.0 - '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - eslint: 8.31.0 - eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) - eslint-plugin-jest-dom: 4.0.3(eslint@8.31.0) - eslint-plugin-jsx-a11y: 6.7.1(eslint@8.31.0) - eslint-plugin-node: 11.1.0(eslint@8.31.0) - eslint-plugin-react: 7.32.2(eslint@8.31.0) - eslint-plugin-react-hooks: 4.6.2(eslint@8.31.0) - eslint-plugin-testing-library: 5.11.0(eslint@8.31.0)(typescript@5.5.4) - react: 18.3.1 - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - jest - - supports-color - '@remix-run/express@2.17.4(express@4.20.0)(typescript@5.5.4)': dependencies: '@remix-run/node': 2.17.4(typescript@5.5.4) @@ -24523,8 +24081,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.1': optional: true - '@rushstack/eslint-patch@1.2.0': {} - '@s2-dev/streamstore@0.22.10(supports-color@10.0.0)': dependencies: '@protobuf-ts/runtime': 2.11.1 @@ -25894,17 +25450,6 @@ snapshots: - bare-buffer - supports-color - '@testing-library/dom@8.19.1': - dependencies: - '@babel/code-frame': 7.24.7 - '@babel/runtime': 7.28.4 - '@types/aria-query': 5.0.1 - aria-query: 5.3.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.15 - lz-string: 1.4.4 - pretty-format: 27.5.1 - '@tokenizer/token@0.3.0': {} '@total-typescript/ts-reset@0.4.2': {} @@ -25922,8 +25467,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - '@types/aria-query@5.0.1': {} - '@types/aws-lambda@8.10.152': {} '@types/bcryptjs@2.4.2': {} @@ -26117,11 +25660,6 @@ snapshots: '@types/eslint': 8.56.12 '@types/estree': 1.0.9 - '@types/eslint@8.4.10': - dependencies: - '@types/estree': 1.0.0 - '@types/json-schema': 7.0.11 - '@types/eslint@8.56.12': dependencies: '@types/estree': 1.0.9 @@ -26131,8 +25669,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - '@types/estree@1.0.0': {} - '@types/estree@1.0.8': {} '@types/estree@1.0.9': {} @@ -26184,10 +25720,6 @@ snapshots: '@types/json-query@2.2.3': {} - '@types/json-schema@7.0.11': {} - - '@types/json-schema@7.0.13': {} - '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -26452,90 +25984,6 @@ snapshots: dependencies: '@types/node': 20.14.14 - '@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4)': - dependencies: - '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - '@typescript-eslint/scope-manager': 5.59.6 - '@typescript-eslint/type-utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - debug: 4.3.4 - eslint: 8.31.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.4 - natural-compare-lite: 1.4.0 - semver: 7.6.3 - tsutils: 3.21.0(typescript@5.5.4) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4)': - dependencies: - '@typescript-eslint/scope-manager': 5.59.6 - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) - debug: 4.4.0 - eslint: 8.31.0 - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@5.59.6': - dependencies: - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/visitor-keys': 5.59.6 - - '@typescript-eslint/type-utils@5.59.6(eslint@8.31.0)(typescript@5.5.4)': - dependencies: - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - debug: 4.4.3(supports-color@10.0.0) - eslint: 8.31.0 - tsutils: 3.21.0(typescript@5.5.4) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@5.59.6': {} - - '@typescript-eslint/typescript-estree@5.59.6(typescript@5.5.4)': - dependencies: - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/visitor-keys': 5.59.6 - debug: 4.4.3(supports-color@10.0.0) - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.7.3 - tsutils: 3.21.0(typescript@5.5.4) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@5.59.6(eslint@8.31.0)(typescript@5.5.4)': - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.31.0) - '@types/json-schema': 7.0.13 - '@types/semver': 7.5.1 - '@typescript-eslint/scope-manager': 5.59.6 - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) - eslint: 8.31.0 - eslint-scope: 5.1.1 - semver: 7.7.3 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/visitor-keys@5.59.6': - dependencies: - '@typescript-eslint/types': 5.59.6 - eslint-visitor-keys: 3.4.2 - '@uiw/codemirror-extensions-basic-setup@4.19.5(@codemirror/autocomplete@6.4.0(@codemirror/language@6.3.2)(@codemirror/state@6.2.0)(@codemirror/view@6.7.2)(@lezer/common@1.3.0))(@codemirror/commands@6.1.3)(@codemirror/language@6.3.2)(@codemirror/lint@6.4.2)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.7.2)': dependencies: '@codemirror/autocomplete': 6.4.0(@codemirror/language@6.3.2)(@codemirror/state@6.2.0)(@codemirror/view@6.7.2)(@lezer/common@1.3.0) @@ -26984,8 +26432,6 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} antlr4ts-cli@0.5.0-alpha.4: {} @@ -27037,10 +26483,6 @@ snapshots: dependencies: tslib: 2.8.1 - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - arktype@2.1.20: dependencies: '@ark/schema': 0.46.0 @@ -27053,26 +26495,8 @@ snapshots: array-flatten@1.1.1: {} - array-includes@3.1.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.0.7 - array-union@2.1.0: {} - array.prototype.findlastindex@1.2.5: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.0.2 - array.prototype.flat@1.3.1: dependencies: call-bind: 1.0.8 @@ -27080,28 +26504,6 @@ snapshots: es-abstract: 1.23.3 es-shim-unscopables: 1.0.2 - array.prototype.flat@1.3.2: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-shim-unscopables: 1.0.2 - - array.prototype.flatmap@1.3.2: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-shim-unscopables: 1.0.2 - - array.prototype.tosorted@1.1.1: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-shim-unscopables: 1.0.2 - get-intrinsic: 1.3.0 - arraybuffer.prototype.slice@1.0.3: dependencies: array-buffer-byte-length: 1.0.1 @@ -27129,8 +26531,6 @@ snapshots: assertion-error@2.0.1: {} - ast-types-flow@0.0.7: {} - ast-v8-to-istanbul@1.0.2: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -27204,8 +26604,6 @@ snapshots: aws4fetch@1.0.18: {} - axe-core@4.6.2: {} - axios@1.16.1: dependencies: follow-redirects: 1.16.0 @@ -27216,10 +26614,6 @@ snapshots: - debug - supports-color - axobject-query@3.2.1: - dependencies: - dequal: 2.0.3 - b4a@1.6.6: {} bail@2.0.2: {} @@ -28253,8 +27647,6 @@ snapshots: d3: 7.9.0 lodash-es: 4.18.1 - damerau-levenshtein@1.0.8: {} - dashdash@1.14.1: dependencies: assert-plus: 1.0.0 @@ -28297,10 +27689,6 @@ snapshots: dependencies: ms: 2.0.0 - debug@3.2.7: - dependencies: - ms: 2.1.3 - debug@4.3.4: dependencies: ms: 2.1.2 @@ -28317,10 +27705,6 @@ snapshots: optionalDependencies: supports-color: 10.0.0 - debug@4.4.0: - dependencies: - ms: 2.1.3 - debug@4.4.3(supports-color@10.0.0): dependencies: ms: 2.1.3 @@ -28353,8 +27737,6 @@ snapshots: deep-extend@0.6.0: {} - deep-is@0.1.4: {} - deep-object-diff@1.1.9: {} deepmerge-ts@7.1.5: {} @@ -28380,8 +27762,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - define-lazy-prop@2.0.0: {} - define-lazy-prop@3.0.0: {} define-properties@1.1.4: @@ -28499,16 +27879,6 @@ snapshots: transitivePeerDependencies: - supports-color - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - - dom-accessibility-api@0.5.15: {} - dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.28.4 @@ -28540,8 +27910,6 @@ snapshots: dependencies: type-fest: 5.6.0 - dotenv@16.0.3: {} - dotenv@16.4.4: {} dotenv@16.4.5: {} @@ -28717,7 +28085,7 @@ snapshots: enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 + tapable: 2.3.3 enquirer@2.3.6: dependencies: @@ -29203,275 +28571,15 @@ snapshots: escape-string-regexp@1.0.5: {} - escape-string-regexp@4.0.0: {} - escape-string-regexp@5.0.0: {} - eslint-config-prettier@8.6.0(eslint@8.31.0): - dependencies: - eslint: 8.31.0 - - eslint-import-resolver-node@0.3.7: - dependencies: - debug: 3.2.7 - is-core-module: 2.14.0 - resolve: 1.22.8 - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-node@0.3.9: - dependencies: - debug: 3.2.7 - is-core-module: 2.14.0 - resolve: 1.22.8 - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0): - dependencies: - debug: 4.4.3(supports-color@10.0.0) - enhanced-resolve: 5.18.3 - eslint: 8.31.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - get-tsconfig: 4.7.6 - globby: 13.2.2 - is-core-module: 2.14.0 - is-glob: 4.0.3 - synckit: 0.8.5 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-node - - eslint-import-resolver-webpack - - supports-color - - eslint-module-utils@2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - eslint: 8.31.0 - eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - eslint: 8.31.0 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) - transitivePeerDependencies: - - supports-color - - eslint-plugin-es@3.0.1(eslint@8.31.0): - dependencies: - eslint: 8.31.0 - eslint-utils: 2.1.0 - regexpp: 3.2.0 - - eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): - dependencies: - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 - array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.31.0 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - hasown: 2.0.2 - is-core-module: 2.14.0 - is-glob: 4.0.3 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.0 - semver: 6.3.1 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-jest-dom@4.0.3(eslint@8.31.0): - dependencies: - '@babel/runtime': 7.28.4 - '@testing-library/dom': 8.19.1 - eslint: 8.31.0 - requireindex: 1.2.0 - - eslint-plugin-jest@26.9.0(@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4): - dependencies: - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - eslint: 8.31.0 - optionalDependencies: - '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) - transitivePeerDependencies: - - supports-color - - typescript - - eslint-plugin-jsx-a11y@6.7.1(eslint@8.31.0): - dependencies: - '@babel/runtime': 7.28.4 - aria-query: 5.3.0 - array-includes: 3.1.8 - array.prototype.flatmap: 1.3.2 - ast-types-flow: 0.0.7 - axe-core: 4.6.2 - axobject-query: 3.2.1 - damerau-levenshtein: 1.0.8 - emoji-regex: 9.2.2 - eslint: 8.31.0 - has: 1.0.3 - jsx-ast-utils: 3.3.3 - language-tags: 1.0.5 - minimatch: 3.1.5 - object.entries: 1.1.6 - object.fromentries: 2.0.8 - semver: 6.3.1 - - eslint-plugin-node@11.1.0(eslint@8.31.0): - dependencies: - eslint: 8.31.0 - eslint-plugin-es: 3.0.1(eslint@8.31.0) - eslint-utils: 2.1.0 - ignore: 5.2.4 - minimatch: 3.1.5 - resolve: 1.22.8 - semver: 6.3.1 - - eslint-plugin-react-hooks@4.6.2(eslint@8.31.0): - dependencies: - eslint: 8.31.0 - - eslint-plugin-react@7.32.2(eslint@8.31.0): - dependencies: - array-includes: 3.1.8 - array.prototype.flatmap: 1.3.2 - array.prototype.tosorted: 1.1.1 - doctrine: 2.1.0 - eslint: 8.31.0 - estraverse: 5.3.0 - jsx-ast-utils: 3.3.3 - minimatch: 3.1.5 - object.entries: 1.1.6 - object.fromentries: 2.0.8 - object.hasown: 1.1.2 - object.values: 1.2.0 - prop-types: 15.8.1 - resolve: 2.0.0-next.4 - semver: 6.3.1 - string.prototype.matchall: 4.0.8 - - eslint-plugin-testing-library@5.11.0(eslint@8.31.0)(typescript@5.5.4): - dependencies: - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - eslint: 8.31.0 - transitivePeerDependencies: - - supports-color - - typescript - - eslint-plugin-turbo@2.0.5(eslint@8.31.0): - dependencies: - dotenv: 16.0.3 - eslint: 8.31.0 - eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 - eslint-scope@7.1.1: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-utils@2.1.0: - dependencies: - eslint-visitor-keys: 1.3.0 - - eslint-utils@3.0.0(eslint@8.31.0): - dependencies: - eslint: 8.31.0 - eslint-visitor-keys: 2.1.0 - - eslint-visitor-keys@1.3.0: {} - - eslint-visitor-keys@2.1.0: {} - - eslint-visitor-keys@3.3.0: {} - - eslint-visitor-keys@3.4.2: {} - - eslint@8.31.0: - dependencies: - '@eslint/eslintrc': 1.4.1 - '@humanwhocodes/config-array': 0.11.8 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.1.1 - eslint-utils: 3.0.0(eslint@8.31.0) - eslint-visitor-keys: 3.3.0 - espree: 9.4.1 - esquery: 1.4.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.19.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.4 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-sdsl: 4.2.0 - js-yaml: 4.1.1 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 - natural-compare: 1.4.0 - optionator: 0.9.1 - regexpp: 3.2.0 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - espree@9.4.1: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 3.4.2 - - espree@9.6.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 3.4.2 - esprima@4.0.1: {} - esquery@1.4.0: - dependencies: - estraverse: 5.3.0 - esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -29515,8 +28623,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - esutils@2.0.3: {} - etag@1.8.1: {} eval@0.1.6: @@ -29788,8 +28894,6 @@ snapshots: json-schema-ref-resolver: 2.0.1 rfdc: 1.4.1 - fast-levenshtein@2.0.6: {} - fast-npm-meta@0.2.2: {} fast-querystring@1.1.2: @@ -29882,10 +28986,6 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.0.4 - file-type@19.6.0: dependencies: get-stream: 9.0.1 @@ -29950,13 +29050,6 @@ snapshots: micromatch: 4.0.8 pkg-dir: 4.2.0 - flat-cache@3.0.4: - dependencies: - flatted: 3.4.2 - rimraf: 3.0.2 - - flatted@3.4.2: {} - follow-redirects@1.16.0: {} for-each@0.3.3: @@ -30229,18 +29322,12 @@ snapshots: globals@11.12.0: {} - globals@13.19.0: - dependencies: - type-fest: 0.20.2 - globals@15.15.0: {} globalthis@1.0.3: dependencies: define-properties: 1.2.1 - globalyzer@0.1.0: {} - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -30767,8 +29854,6 @@ snapshots: is-deflate@1.0.0: {} - is-docker@2.2.1: {} - is-docker@3.0.0: {} is-electron@2.2.2: {} @@ -30807,8 +29892,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-plain-obj@1.1.0: {} is-plain-obj@3.0.0: {} @@ -30870,10 +29953,6 @@ snapshots: is-windows@1.0.2: {} - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -30966,8 +30045,6 @@ snapshots: js-levenshtein@1.1.6: {} - js-sdsl@4.2.0: {} - js-tokens@10.0.0: {} js-tokens@4.0.0: {} @@ -31011,8 +30088,6 @@ snapshots: json-schema@0.4.0: {} - json-stable-stringify-without-jsonify@1.0.1: {} - json-stable-stringify@1.3.0: dependencies: call-bind: 1.0.8 @@ -31073,11 +30148,6 @@ snapshots: json-schema: 0.4.0 verror: 1.10.0 - jsx-ast-utils@3.3.3: - dependencies: - array-includes: 3.1.8 - object.assign: 4.1.5 - junk@4.0.1: {} jwa@1.4.2: @@ -31120,12 +30190,6 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 - language-subtag-registry@0.3.22: {} - - language-tags@1.0.5: - dependencies: - language-subtag-registry: 0.3.22 - layerr@2.0.1: {} layout-base@1.0.2: {} @@ -31138,54 +30202,6 @@ snapshots: leac@0.6.0: {} - lefthook-darwin-arm64@1.11.3: - optional: true - - lefthook-darwin-x64@1.11.3: - optional: true - - lefthook-freebsd-arm64@1.11.3: - optional: true - - lefthook-freebsd-x64@1.11.3: - optional: true - - lefthook-linux-arm64@1.11.3: - optional: true - - lefthook-linux-x64@1.11.3: - optional: true - - lefthook-openbsd-arm64@1.11.3: - optional: true - - lefthook-openbsd-x64@1.11.3: - optional: true - - lefthook-windows-arm64@1.11.3: - optional: true - - lefthook-windows-x64@1.11.3: - optional: true - - lefthook@1.11.3: - optionalDependencies: - lefthook-darwin-arm64: 1.11.3 - lefthook-darwin-x64: 1.11.3 - lefthook-freebsd-arm64: 1.11.3 - lefthook-freebsd-x64: 1.11.3 - lefthook-linux-arm64: 1.11.3 - lefthook-linux-x64: 1.11.3 - lefthook-openbsd-arm64: 1.11.3 - lefthook-openbsd-x64: 1.11.3 - lefthook-windows-arm64: 1.11.3 - lefthook-windows-x64: 1.11.3 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - light-my-request@6.6.0: dependencies: cookie: 1.0.2 @@ -31379,8 +30395,6 @@ snapshots: luxon@3.2.1: {} - lz-string@1.4.4: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -32410,10 +31424,6 @@ snapshots: napi-build-utils@2.0.0: optional: true - natural-compare-lite@1.4.0: {} - - natural-compare@1.4.0: {} - nearley@2.20.1: dependencies: commander: 2.20.3 @@ -32612,36 +31622,6 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 - object.entries@1.1.6: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-object-atoms: 1.1.1 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - - object.hasown@1.1.2: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.23.3 - - object.values@1.2.0: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - obuf@1.1.2: {} obug@2.1.1: {} @@ -32708,12 +31688,6 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 - open@8.4.0: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - openai@4.33.1(encoding@0.1.13): dependencies: '@types/node': 20.14.14 @@ -32755,15 +31729,6 @@ snapshots: jose: 6.0.8 oauth4webapi: 3.3.0 - optionator@0.9.1: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.3 - ora@5.4.1: dependencies: bl: 4.1.0 @@ -32784,6 +31749,52 @@ snapshots: outdent@0.8.0: {} + oxfmt@0.54.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.54.0 + '@oxfmt/binding-android-arm64': 0.54.0 + '@oxfmt/binding-darwin-arm64': 0.54.0 + '@oxfmt/binding-darwin-x64': 0.54.0 + '@oxfmt/binding-freebsd-x64': 0.54.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.54.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.54.0 + '@oxfmt/binding-linux-arm64-gnu': 0.54.0 + '@oxfmt/binding-linux-arm64-musl': 0.54.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.54.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.54.0 + '@oxfmt/binding-linux-riscv64-musl': 0.54.0 + '@oxfmt/binding-linux-s390x-gnu': 0.54.0 + '@oxfmt/binding-linux-x64-gnu': 0.54.0 + '@oxfmt/binding-linux-x64-musl': 0.54.0 + '@oxfmt/binding-openharmony-arm64': 0.54.0 + '@oxfmt/binding-win32-arm64-msvc': 0.54.0 + '@oxfmt/binding-win32-ia32-msvc': 0.54.0 + '@oxfmt/binding-win32-x64-msvc': 0.54.0 + + oxlint@1.70.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.70.0 + '@oxlint/binding-android-arm64': 1.70.0 + '@oxlint/binding-darwin-arm64': 1.70.0 + '@oxlint/binding-darwin-x64': 1.70.0 + '@oxlint/binding-freebsd-x64': 1.70.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.70.0 + '@oxlint/binding-linux-arm-musleabihf': 1.70.0 + '@oxlint/binding-linux-arm64-gnu': 1.70.0 + '@oxlint/binding-linux-arm64-musl': 1.70.0 + '@oxlint/binding-linux-ppc64-gnu': 1.70.0 + '@oxlint/binding-linux-riscv64-gnu': 1.70.0 + '@oxlint/binding-linux-riscv64-musl': 1.70.0 + '@oxlint/binding-linux-s390x-gnu': 1.70.0 + '@oxlint/binding-linux-x64-gnu': 1.70.0 + '@oxlint/binding-linux-x64-musl': 1.70.0 + '@oxlint/binding-openharmony-arm64': 1.70.0 + '@oxlint/binding-win32-arm64-msvc': 1.70.0 + '@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: @@ -33367,26 +32378,12 @@ snapshots: path-exists: 4.0.0 which-pm: 2.0.0 - prelude-ls@1.2.1: {} - prepend-http@2.0.0: {} - prettier-plugin-tailwindcss@0.3.0(prettier@2.8.8): - dependencies: - prettier: 2.8.8 - prettier@2.8.8: {} - prettier@3.0.0: {} - prettier@3.8.3: {} - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - pretty-hrtime@1.0.3: {} pretty-ms@7.0.1: @@ -33731,8 +32728,6 @@ snapshots: react-is@16.13.1: {} - react-is@17.0.2: {} - react-is@18.3.1: {} react-markdown@10.1.0(@types/react@18.2.69)(react@18.3.1): @@ -34036,8 +33031,6 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 - regexpp@3.2.0: {} - registry-auth-token@4.2.2: dependencies: rc: 1.2.8 @@ -34245,8 +33238,6 @@ snapshots: require-main-filename@2.0.0: {} - requireindex@1.2.0: {} - resend@3.2.0: dependencies: '@react-email/render': 0.0.12 @@ -34272,12 +33263,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.4: - dependencies: - is-core-module: 2.14.0 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - responselike@1.0.2: dependencies: lowercase-keys: 1.0.1 @@ -34301,10 +33286,6 @@ snapshots: rfdc@1.4.1: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - rimraf@6.0.1: dependencies: glob: 11.1.0 @@ -34945,17 +33926,6 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 - string.prototype.matchall@4.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - internal-slot: 1.0.7 - regexp.prototype.flags: 1.5.2 - side-channel: 1.1.0 - string.prototype.padend@3.1.4: dependencies: call-bind: 1.0.7 @@ -35028,8 +33998,6 @@ snapshots: strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} - strnum@1.0.5: {} strnum@2.2.3: {} @@ -35143,11 +34111,6 @@ snapshots: rimraf: 6.0.1 tshy: 3.0.2 - synckit@0.8.5: - dependencies: - '@pkgr/utils': 2.3.1 - tslib: 2.8.1 - systeminformation@5.31.7: {} table@6.9.0: @@ -35232,8 +34195,6 @@ snapshots: tailwindcss@4.3.0: {} - tapable@2.3.0: {} - tapable@2.3.3: {} tar-fs@2.1.3: @@ -35353,8 +34314,6 @@ snapshots: dependencies: b4a: 1.6.6 - text-table@0.2.0: {} - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -35378,11 +34337,6 @@ snapshots: tiny-case@1.0.3: {} - tiny-glob@0.2.9: - dependencies: - globalyzer: 0.1.0 - globrex: 0.1.2 - tiny-invariant@1.3.1: {} tinybench@2.9.0: {} @@ -35422,6 +34376,8 @@ snapshots: '@types/tinycolor2': 1.4.3 tinycolor2: 1.6.0 + tinypool@2.1.0: {} + tinyrainbow@3.1.0: {} tldts-core@7.0.7: {} @@ -35529,13 +34485,6 @@ snapshots: minimist: 1.2.7 strip-bom: 3.0.0 - tsconfig-paths@3.15.0: - 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 @@ -35622,11 +34571,6 @@ snapshots: - tsx - yaml - tsutils@3.21.0(typescript@5.5.4): - dependencies: - tslib: 1.14.1 - typescript: 5.5.4 - tsx@3.12.2: dependencies: '@esbuild-kit/cjs-loader': 2.4.1 @@ -35707,14 +34651,8 @@ snapshots: tweetnacl@0.14.5: {} - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - type-fest@0.13.1: {} - type-fest@0.20.2: {} - type-fest@0.6.0: {} type-fest@0.8.1: {} @@ -36408,8 +35346,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - word-wrap@1.2.3: {} - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index 68de2fe5c9..0000000000 --- a/prettier.config.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - semi: true, - singleQuote: false, - jsxSingleQuote: false, - trailingComma: "es5", - bracketSpacing: true, - bracketSameLine: false, - printWidth: 100, - tabWidth: 2, - useTabs: false, -}; diff --git a/rules/manifest.json b/rules/manifest.json index 64d9a86139..d7bc0a9168 100644 --- a/rules/manifest.json +++ b/rules/manifest.json @@ -151,4 +151,4 @@ ] } } -} \ No newline at end of file +} diff --git a/scripts/bundleSdkDocs.ts b/scripts/bundleSdkDocs.ts index 80baf7ef0e..bcbeb10723 100644 --- a/scripts/bundleSdkDocs.ts +++ b/scripts/bundleSdkDocs.ts @@ -114,7 +114,9 @@ async function bundleSdkDocs() { if (copied === 0) { // Every nav page was missing on disk; refuse to ship the SDK with an empty docs bundle. - throw new Error(`[bundleSdkDocs] 0 docs copied from the "${DROPDOWN}" nav; refusing empty docs bundle`); + throw new Error( + `[bundleSdkDocs] 0 docs copied from the "${DROPDOWN}" nav; refusing empty docs bundle` + ); } console.log( diff --git a/scripts/generate-github-release.mjs b/scripts/generate-github-release.mjs index fda2f3cb8c..8fc528df63 100755 --- a/scripts/generate-github-release.mjs +++ b/scripts/generate-github-release.mjs @@ -88,9 +88,7 @@ function extractChangesFromPrBody(body) { function getContributors(previousVersion) { try { - const range = previousVersion - ? `v${previousVersion}...HEAD` - : "HEAD~50..HEAD"; + const range = previousVersion ? `v${previousVersion}...HEAD` : "HEAD~50..HEAD"; const log = execSync(`git log ${range} --format="%aN|%aE" --no-merges`, { cwd: ROOT_DIR, encoding: "utf-8", @@ -111,9 +109,7 @@ function getContributors(previousVersion) { contributors.set(name, (contributors.get(name) || 0) + 1); } - return [...contributors.entries()] - .sort((a, b) => b[1] - a[1]) - .map(([name]) => name); + return [...contributors.entries()].sort((a, b) => b[1] - a[1]).map(([name]) => name); } catch { return []; } @@ -128,9 +124,7 @@ function getPublishedPackages() { for (const dir of readdirSync(packagesDir, { withFileTypes: true })) { if (!dir.isDirectory()) continue; try { - const pkg = JSON.parse( - readFileSync(join(packagesDir, dir.name, "package.json"), "utf-8") - ); + const pkg = JSON.parse(readFileSync(join(packagesDir, dir.name, "package.json"), "utf-8")); if (pkg.name && !pkg.private) { names.push(pkg.name); } @@ -217,9 +211,7 @@ function formatRelease({ version, changesContent, contributors, packages }) { lines.push("## Contributors"); lines.push(""); lines.push( - contributors - .map((c) => (/^[A-Za-z0-9][-A-Za-z0-9]*$/.test(c) ? `@${c}` : c)) - .join(", ") + contributors.map((c) => (/^[A-Za-z0-9][-A-Za-z0-9]*$/.test(c) ? `@${c}` : c)).join(", ") ); lines.push(""); } diff --git a/scripts/recover-stuck-runs.ts b/scripts/recover-stuck-runs.ts index 28bb4e85e4..e107cc3854 100755 --- a/scripts/recover-stuck-runs.ts +++ b/scripts/recover-stuck-runs.ts @@ -71,18 +71,20 @@ async function main() { const [environmentId, postgresUrl, redisReadUrl, redisWriteUrl] = process.argv.slice(2); if (!environmentId || !postgresUrl || !redisReadUrl) { - console.error("Usage: tsx scripts/recover-stuck-runs.ts [redisWriteUrl]"); + console.error( + "Usage: tsx scripts/recover-stuck-runs.ts [redisWriteUrl]" + ); console.error(""); console.error("Dry-run mode when no redisWriteUrl is provided (read-only)."); console.error("Execute mode when redisWriteUrl is provided (makes actual changes)."); console.error(""); console.error("Example (dry-run):"); - console.error(' tsx scripts/recover-stuck-runs.ts env_1234567890 \\'); + console.error(" tsx scripts/recover-stuck-runs.ts env_1234567890 \\"); console.error(' "postgresql://user:pass@localhost:5432/triggerdev" \\'); console.error(' "redis://readonly.example.com:6379"'); console.error(""); console.error("Example (execute):"); - console.error(' tsx scripts/recover-stuck-runs.ts env_1234567890 \\'); + console.error(" tsx scripts/recover-stuck-runs.ts env_1234567890 \\"); console.error(' "postgresql://user:pass@localhost:5432/triggerdev" \\'); console.error(' "redis://readonly.example.com:6379" \\'); console.error(' "redis://writeonly.example.com:6379"'); @@ -261,7 +263,9 @@ async function main() { } // Prepare recovery operations - console.log(`\n⚑ ${executeMode ? "Executing" : "Planning"} recovery for ${stuckRuns.length} stuck runs`); + console.log( + `\n⚑ ${executeMode ? "Executing" : "Planning"} recovery for ${stuckRuns.length} stuck runs` + ); console.log(`This will:`); console.log(` 1. Add each run back to its specific queue sorted set`); console.log(` 2. Remove each run from the queue-specific currentConcurrency set`); diff --git a/scripts/stamp-preview-version.mjs b/scripts/stamp-preview-version.mjs index a6b21d1077..36a025668f 100644 --- a/scripts/stamp-preview-version.mjs +++ b/scripts/stamp-preview-version.mjs @@ -55,9 +55,7 @@ function resolveSha() { const sha = resolveSha().slice(0, 7); const previewVersion = `0.0.0-preview-${sha}`; -const dirs = readdirSync(PACKAGES_DIR, { withFileTypes: true }).filter((e) => - e.isDirectory() -); +const dirs = readdirSync(PACKAGES_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()); // First pass: collect every public package name so we know which workspace // specifiers point at a sibling whose version we are about to change. diff --git a/scripts/upgrade-package.mjs b/scripts/upgrade-package.mjs deleted file mode 100644 index c1245cf9f7..0000000000 --- a/scripts/upgrade-package.mjs +++ /dev/null @@ -1,123 +0,0 @@ -import { promises as fs } from "fs"; -import { join } from "path"; -import { exec } from "child_process"; -import prettier from "prettier"; - -import prettierConfig from "../prettier.config.js"; - -async function runShellCommand(command, directory) { - return new Promise((resolve, reject) => { - exec(command, { cwd: directory }, (error, stdout, stderr) => { - if (error) { - console.error(`Error: ${error.message}`); - reject(error); - return; - } - if (stderr) { - console.error(`Stderr: ${stderr}`); - reject(stderr); - return; - } - console.log(`Stdout: ${stdout}`); - resolve(stdout); - }); - }); -} - -async function updatePackage(directory) { - // Updating package.json - const packageJsonPath = join(directory, "package.json"); - const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); - - // Updating dependencies - packageJson.devDependencies = packageJson.devDependencies || {}; - packageJson.devDependencies["@trigger.dev/tsup"] = "workspace:*"; - packageJson.devDependencies["tsup"] = "8.0.1"; - packageJson.devDependencies["typescript"] = "^5.3.0"; - - // Updating exports, main, types, and module - packageJson.exports = { - ".": { - import: { - types: "./dist/index.d.mts", - default: "./dist/index.mjs", - }, - require: "./dist/index.js", - types: "./dist/index.d.ts", - }, - "./package.json": "./package.json", - }; - packageJson.main = "./dist/index.js"; - packageJson.types = "./dist/index.d.ts"; - packageJson.module = "./dist/index.mjs"; - packageJson.files = ["dist"]; - - await fs.writeFile( - packageJsonPath, - await prettier.format(JSON.stringify(packageJson, null, 2), { - parser: "json", - ...prettierConfig, - }) - ); - - console.log(`βœ… Updated package.json for ${packageJson.name}`); - - // Updating tsconfig.json - const tsconfigPath = join(directory, "tsconfig.json"); - const tsconfig = JSON.parse(await fs.readFile(tsconfigPath, "utf8")); - - if (tsconfig.extends === "@trigger.dev/tsconfig/integration.json") { - console.log( - `βœ… tsconfig.json for ${packageJson.name} already extends @trigger.dev/tsconfig/integration.json` - ); - } else { - tsconfig.compilerOptions = tsconfig.compilerOptions || {}; - tsconfig.compilerOptions.paths = { - ...tsconfig.compilerOptions.paths, - "@trigger.dev/tsup/*": ["../../config-packages/tsup/src/*"], - "@trigger.dev/tsup": ["../../config-packages/tsup/src/index"], - }; - - await fs.writeFile( - tsconfigPath, - await prettier.format(JSON.stringify(tsconfig, null, 2), { - parser: "json", - ...prettierConfig, - }) - ); - - console.log(`βœ… Updated tsconfig.json for ${packageJson.name}`); - } - - // Updating tsup.config.ts - const tsupConfigPath = join(directory, "tsup.config.ts"); - const tsupConfigContent = `import { defineConfigPackage } from "@trigger.dev/tsup";\n\nexport default defineConfigPackage;`; - await fs.writeFile( - tsupConfigPath, - await prettier.format(tsupConfigContent, { parser: "typescript", ...prettierConfig }) - ); - - console.log(`βœ… Updated tsup.config.ts for ${packageJson.name}`); - - console.log(`βœ… Updated package ${packageJson.name}, now running pnpm install and build`); - - await runShellCommand("pnpm install", process.cwd()); - await runShellCommand(`pnpm run build --filter ${packageJson.name}`, process.cwd()); -} - -async function main() { - const packagePath = process.argv[2]; - - if (!packagePath) { - throw new Error("Missing package path"); - } - - console.log(`Updating package ${packagePath}`); - - await updatePackage(packagePath); -} - -main().catch((error) => { - console.error(error); - process.exit(1); -}); diff --git a/server.json b/server.json index 57b89ff193..b0b552e33a 100644 --- a/server.json +++ b/server.json @@ -28,4 +28,4 @@ "environment_variables": [] } ] -} \ No newline at end of file +} diff --git a/turbo.json b/turbo.json index 8f2c862d03..c820d11537 100644 --- a/turbo.json +++ b/turbo.json @@ -2,32 +2,16 @@ "$schema": "https://turborepo.org/schema.json", "pipeline": { "build": { - "dependsOn": [ - "^build" - ], - "outputs": [ - "dist/**", - "public/build/**", - "build/**", - "app/styles/tailwind.css", - ".cache" - ] + "dependsOn": ["^build"], + "outputs": ["dist/**", "public/build/**", "build/**", "app/styles/tailwind.css", ".cache"] }, "webapp#start": { - "dependsOn": [ - "^build" - ], - "outputs": [ - "public/build/**" - ] + "dependsOn": ["^build"], + "outputs": ["public/build/**"] }, "start": { - "dependsOn": [ - "^build" - ], - "outputs": [ - "public/build/**" - ] + "dependsOn": ["^build"], + "outputs": ["public/build/**"] }, "db:generate": { "cache": false @@ -37,9 +21,7 @@ }, "webapp#db:seed": { "cache": false, - "dependsOn": [ - "webapp#build" - ] + "dependsOn": ["webapp#build"] }, "db:studio": { "cache": false @@ -49,29 +31,20 @@ }, "dev": { "cache": false, - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] }, "i:dev": { "cache": false }, "generate": { - "dependsOn": [ - "^generate" - ] - }, - "lint": { - "outputs": [] + "dependsOn": ["^generate"] }, "docker:build": { "outputs": [], "cache": false }, "test": { - "dependsOn": [ - "^build" - ], + "dependsOn": ["^build"], "outputs": [] }, "test:dev": { @@ -79,28 +52,20 @@ "cache": false }, "test:e2e:dev": { - "dependsOn": [ - "^build" - ], + "dependsOn": ["^build"], "outputs": [], "cache": false }, "test:e2e:ci": { - "dependsOn": [ - "^build" - ], + "dependsOn": ["^build"], "outputs": [] }, "typecheck": { - "dependsOn": [ - "^build" - ], + "dependsOn": ["^build"], "outputs": [] }, "check-exports": { - "dependsOn": [ - "^build" - ], + "dependsOn": ["^build"], "outputs": [] }, "clean": { @@ -113,9 +78,7 @@ "cache": false } }, - "globalDependencies": [ - ".env" - ], + "globalDependencies": [".env"], "globalEnv": [ "NODE_ENV", "REMIX_APP_PORT", @@ -139,4 +102,4 @@ "APP_ENV", "APP_LOG_LEVEL" ] -} \ No newline at end of file +}