From 627e05929828f139073196050450aed1e3a0e60a Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sun, 17 May 2026 05:55:12 +0100 Subject: [PATCH 1/2] feat(trigger-sdk): add streamBaseURL to TriggerChatTransport (#3641) `TriggerChatTransport` had a single `baseURL` option covering both the `.in/append` POSTs and the long-lived `.out` SSE subscription. Customers wanting to route the SSE through a proxy (e.g. a Cloudflare worker capturing JA4 fingerprints for bot detection) had to send every append through the proxy too, adding a hop to every user message. New optional `streamBaseURL` overrides the SSE base URL only; appends keep using `baseURL`. Falls back to `baseURL` when unset, so existing transports are unchanged. ```ts const transport = new TriggerChatTransport({ task: "ai-chat", baseURL: "https://api.trigger.dev", streamBaseURL: "https://chat-proxy.example.com", accessToken, startSession, }); ``` Verified with a new test in `chat.test.ts` that asserts `.in/append` routes through `baseURL` and `.out` SSE routes through `streamBaseURL`. All existing tests still pass. --- packages/trigger-sdk/src/v3/chat.test.ts | 34 ++++++++++++++++++++++++ packages/trigger-sdk/src/v3/chat.ts | 13 ++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/trigger-sdk/src/v3/chat.test.ts b/packages/trigger-sdk/src/v3/chat.test.ts index eaa69bed934..6f7664da229 100644 --- a/packages/trigger-sdk/src/v3/chat.test.ts +++ b/packages/trigger-sdk/src/v3/chat.test.ts @@ -568,6 +568,40 @@ describe("TriggerChatTransport", () => { expect(subscribe!).toContain("/realtime/v1/sessions/chat-by-chatid/out"); }); + it("routes .out SSE through streamBaseURL while appends stay on baseURL", async () => { + const requests: string[] = []; + global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { + const urlStr = typeof url === "string" ? url : url.toString(); + requests.push(urlStr); + if (isSessionStreamAppendUrl(urlStr)) return defaultAppendResponse(); + if (isSessionOutSubscribeUrl(urlStr)) return defaultSseResponse(); + throw new Error(`Unexpected URL: ${urlStr}`); + }); + + const transport = new TriggerChatTransport({ + task: "my-chat-task", + accessToken: () => "pat", + baseURL: "https://api.test.trigger.dev", + streamBaseURL: "https://chat-proxy.example.com", + sessions: { "chat-split": { publicAccessToken: "p" } }, + }); + + const stream = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-split", + messageId: undefined, + messages: [createUserMessage("Hi")], + abortSignal: undefined, + }); + await drainChunks(stream); + + const append = requests.find(isSessionStreamAppendUrl); + const subscribe = requests.find(isSessionOutSubscribeUrl); + expect(append!.startsWith("https://api.test.trigger.dev/")).toBe(true); + expect(subscribe!.startsWith("https://chat-proxy.example.com/")).toBe(true); + expect(subscribe!).toContain("/realtime/v1/sessions/chat-split/out"); + }); + it("for submit-message, only the latest message is delivered to .in", async () => { // Slim wire: each `.in/append` carries at most ONE new message in // `payload.message` (singular). Even if the caller hands sendMessages diff --git a/packages/trigger-sdk/src/v3/chat.ts b/packages/trigger-sdk/src/v3/chat.ts index 980d34c1f04..b2905bf88d4 100644 --- a/packages/trigger-sdk/src/v3/chat.ts +++ b/packages/trigger-sdk/src/v3/chat.ts @@ -225,6 +225,15 @@ export type TriggerChatTransportOptions = { /** Base URL for the Trigger.dev API. @default "https://api.trigger.dev" */ baseURL?: string; + /** + * Base URL for the SSE stream subscription only (`GET .../sessions/{chatId}/out`). + * Falls back to `baseURL` when unset. Set this to route the long-lived + * stream through a custom proxy (e.g. a Cloudflare worker capturing JA4 + * fingerprints for bot detection) while keeping append POSTs direct to + * `baseURL` to avoid an extra hop on every user message. + */ + streamBaseURL?: string; + /** Additional headers included in every API request. */ headers?: Record; @@ -346,6 +355,7 @@ export class TriggerChatTransport implements ChatTransport { | ((params: StartSessionParams>) => Promise) | undefined; private readonly baseURL: string; + private readonly streamBaseURL: string; private readonly extraHeaders: Record; private readonly streamTimeoutSeconds: number; private defaultMetadata: Record | undefined; @@ -367,6 +377,7 @@ export class TriggerChatTransport implements ChatTransport { | ((params: StartSessionParams>) => Promise) | undefined; this.baseURL = options.baseURL ?? DEFAULT_BASE_URL; + this.streamBaseURL = options.streamBaseURL ?? this.baseURL; this.extraHeaders = options.headers ?? {}; this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS; this.defaultMetadata = options.clientData; @@ -1021,7 +1032,7 @@ export class TriggerChatTransport implements ChatTransport { ); } - const streamUrl = `${this.baseURL}/realtime/v1/sessions/${encodeURIComponent(chatId)}/out`; + const streamUrl = `${this.streamBaseURL}/realtime/v1/sessions/${encodeURIComponent(chatId)}/out`; return new ReadableStream({ start: async (controller) => { From 55fa2d4967ea97beae477cf2dbb33627f3626857 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sun, 17 May 2026 09:28:57 +0100 Subject: [PATCH 2/2] fix(cli): TRIGGER_BUILD_SKIP_REWRITE_TIMESTAMP escape hatch for local self-hosted builds (#3618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Local self-hosted deploys (`trigger deploy --local-build --push --builder orbstack` or any other buildx setup using the **docker** driver) fail at the push step with: ``` ERROR: failed to build: failed to solve: exporter option "rewrite-timestamp" conflicts with "unpack" ``` The docker driver auto-enables `unpack=true` when pushing, and that's incompatible with `rewrite-timestamp` (which the CLI sets for reproducible-build hashing). Adds a simple env-var opt-out so contributors can keep using their default builder. The flag is only read by the local-build code path; remote/cloud builds are unaffected. ```bash TRIGGER_BUILD_SKIP_REWRITE_TIMESTAMP=1 \ pnpm exec trigger deploy --profile default --local-build --push --builder orbstack ``` The trade-off: skipping `rewrite-timestamp` means layer timestamps reflect actual build time, so two identical builds produce different layer hashes. Fine for a local-dev registry; the only real consumer of timestamp-stability is registry-layer cache hit rates. ## Test plan - [x] Manual: ran `trigger deploy --profile default --local-build --push --builder orbstack` against the localhost webapp + a local Docker registry on port 5001 — first failed with the rewrite-timestamp/unpack error, then succeeded after setting `TRIGGER_BUILD_SKIP_REWRITE_TIMESTAMP=1`. - [x] Full chat.agent smoke sweep (15 tests, including suspend/resume, deepResearch subtask, AgentChat orchestrator) against the deployed image — all pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .changeset/cli-deploy-skip-rewrite-timestamp.md | 5 +++++ packages/cli-v3/src/deploy/buildImage.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .changeset/cli-deploy-skip-rewrite-timestamp.md diff --git a/.changeset/cli-deploy-skip-rewrite-timestamp.md b/.changeset/cli-deploy-skip-rewrite-timestamp.md new file mode 100644 index 00000000000..60e82732dce --- /dev/null +++ b/.changeset/cli-deploy-skip-rewrite-timestamp.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +Add `TRIGGER_BUILD_SKIP_REWRITE_TIMESTAMP=1` escape hatch for local self-hosted builds whose buildx driver doesn't support `rewrite-timestamp` alongside push (e.g. orbstack's default `docker` driver). diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 31a2b658545..aa8285a7c3e 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -1152,7 +1152,14 @@ function getOutputOptions({ return outputOptions; } - const outputOptions: string[] = ["type=image", "oci-mediatypes=true", "rewrite-timestamp=true"]; + // `rewrite-timestamp` is incompatible with the buildx docker driver's + // implicit `unpack=true` on push (used by e.g. orbstack's default builder). + // Provide an env-var escape hatch so local-dev deploys can opt out. + const skipRewriteTimestamp = process.env.TRIGGER_BUILD_SKIP_REWRITE_TIMESTAMP === "1"; + const outputOptions: string[] = ["type=image", "oci-mediatypes=true"]; + if (!skipRewriteTimestamp) { + outputOptions.push("rewrite-timestamp=true"); + } if (imageTag) { outputOptions.push(`name=${imageTag}`);