Skip to content

Commit 0ffb44c

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/gen2-minting-tri-13430
# Conflicts: # apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.waitpoints.tokens.$waitpointFriendlyId.wait.ts
2 parents 268b6cd + c115f44 commit 0ffb44c

135 files changed

Lines changed: 17052 additions & 1347 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/fluffy-pans-argue.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
---
4+
5+
Fix chat transport discarding the next turn after stopping generation. `skipToTurnComplete` is now reset when a new message or action is sent, so a message sent after `stopGeneration` streams normally instead of leaving the chat stuck in a streaming state.

.changeset/tidy-mailboxes-wait.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@trigger.dev/core": patch
3+
"@trigger.dev/sdk": patch
4+
---
5+
6+
Fixes a case where a chat could silently lose a message. If a message arrived while the agent was between turns and a stop arrived after it, the cursor the next boot resumed from could point past that message, so it was never answered and no error was raised. This affected `chat.agent`, not just custom agents.
7+
8+
Fixes a recovered answer being cut off. After a crash the agent replays the message it had not answered yet, but it was replaying the stop that arrived after that message too, so the turn answering it was aborted the moment it began. A stop is now only applied to the turn that was live when it arrived. That holds however the stop got there: sent after the last completed turn, or sent to a chat whose most recent turn was completed by an older version of the SDK.
9+
10+
One limitation to know about: the recovered answer is persisted correctly, but a chat page that stayed open across the crash keeps showing the partial answer it had already received. Reload the page to see the full recovered answer.
11+
12+
Also fixes a retried send being answered twice. When a send was retried and its idempotency claim was lost, the agent could consume the same message a second time.
13+
14+
Custom agent loops can now inspect pending chat input without consuming it, and consume one record at a time, with `chat.messages.hasPending()` and `chat.messages.next()`. Records carry stable identifiers so a redelivery is recognisable.
15+
16+
```ts
17+
if (await chat.messages.hasPending()) {
18+
const record = await chat.messages.next({ timeoutInSeconds: 0 });
19+
if (record) handle(record.payload);
20+
}
21+
```
22+
23+
`hasPending()` answers for messages alone, so a message sitting behind a stop, or behind a record this version of the SDK does not recognise, still reports as pending and is still delivered. Anything the agent has no consumer for is discarded rather than left where it would make every message queued behind it undeliverable. `chat.messages.next()` returning `undefined` means no message became consumable before the timeout.
24+
25+
`chat.writeTurnComplete()`'s `sessionInEventId` is the cursor that is safe to resume from, not the sequence of the record the turn answered. It is held back behind any message still waiting to be handled, so a value below the record you just handled is expected.

.changeset/violet-buses-tease.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Send the CLI version header on all API requests so deployments are attributable to a CLI version
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
The Staging branch setting now shows an upgrade prompt on plans that don't include a Staging environment, instead of looking editable and then silently doing nothing when saved.

.server-changes/waitpoint-token-wait-404.md

Lines changed: 0 additions & 6 deletions
This file was deleted.

apps/webapp/app/components/integrations/VercelBuildSettings.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ type BuildSettingsFieldsProps = {
4949
* the pin status is unknown — distinct from "not set". */
5050
currentTriggerVersionFetchFailed?: boolean;
5151
/** Hide the section-level master toggles for "Pull env vars" and "Discover new env vars". */
52-
hideSectionToggles?: boolean;
5352
showAtomicDeployments?: boolean;
5453
layout?: "settings" | "card";
5554
};
@@ -68,7 +67,6 @@ export function BuildSettingsFields({
6867
onAutoPromoteChange,
6968
currentTriggerVersion,
7069
currentTriggerVersionFetchFailed,
71-
hideSectionToggles,
7270
showAtomicDeployments = true,
7371
layout = "card",
7472
}: BuildSettingsFieldsProps) {
@@ -222,7 +220,7 @@ export function BuildSettingsFields({
222220
<div className="mb-2">
223221
<div className="flex items-center justify-between">
224222
<Label>Pull env vars before build</Label>
225-
{!hideSectionToggles && availableEnvSlugs.length > 1 && (
223+
{availableEnvSlugs.length > 1 && (
226224
<Switch
227225
variant="small"
228226
checked={
@@ -292,7 +290,7 @@ export function BuildSettingsFields({
292290
<div className="mb-2">
293291
<div className="flex items-center justify-between">
294292
<Label>Discover new env vars</Label>
295-
{!hideSectionToggles && availableEnvSlugs.length > 1 && (
293+
{availableEnvSlugs.length > 1 && (
296294
<Switch
297295
variant="small"
298296
checked={

apps/webapp/app/components/integrations/VercelOnboardingModal.tsx

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
ChevronDownIcon,
55
ChevronUpIcon,
66
} from "@heroicons/react/20/solid";
7-
import { useFetcher, useNavigation, useSearchParams } from "@remix-run/react";
7+
import { useFetcher, useSearchParams } from "@remix-run/react";
88
import { useTypedFetcher } from "remix-typedjson";
99
import { Dialog, DialogContent, DialogHeader } from "~/components/primitives/Dialog";
1010
import { Button, LinkButton } from "~/components/primitives/Buttons";
@@ -34,13 +34,11 @@ import {
3434
type EnvSlug,
3535
ALL_ENV_SLUGS,
3636
shouldSyncEnvVarForAnyEnvironment,
37-
getAvailableEnvSlugs,
3837
getAvailableEnvSlugsForBuildSettings,
3938
} from "~/v3/vercel/vercelProjectIntegrationSchema";
4039
import { type VercelCustomEnvironment } from "~/models/vercelIntegration.server";
4140
import { type VercelOnboardingData } from "~/presenters/v3/VercelSettingsPresenter.server";
4241
import {
43-
vercelAppInstallPath,
4442
v3ProjectSettingsIntegrationsPath,
4543
githubAppInstallPath,
4644
vercelResourcePath,
@@ -78,7 +76,6 @@ function formatVercelTargets(targets: string[]): string {
7876

7977
type OnboardingState =
8078
| "idle"
81-
| "installing"
8279
| "loading-projects"
8380
| "project-selection"
8481
| "loading-env-mapping"
@@ -99,6 +96,7 @@ export function VercelOnboardingModal({
9996
hasStagingEnvironment,
10097
hasPreviewEnvironment,
10198
hasOrgIntegration,
99+
onboardingDataUnavailable = false,
102100
nextUrl,
103101
onDataReload,
104102
vercelManageAccessUrl,
@@ -112,16 +110,15 @@ export function VercelOnboardingModal({
112110
hasStagingEnvironment: boolean;
113111
hasPreviewEnvironment: boolean;
114112
hasOrgIntegration: boolean;
113+
onboardingDataUnavailable?: boolean;
115114
nextUrl?: string;
116115
onDataReload?: (vercelStagingEnvironment?: string) => void;
117116
vercelManageAccessUrl?: string;
118117
}) {
119118
const { capture, startSessionRecording } = usePostHogTracking();
120-
const navigation = useNavigation();
121119
const fetcher = useTypedFetcher<typeof loader>();
122120
const envMappingFetcher = useFetcher();
123121
const completeOnboardingFetcher = useFetcher();
124-
const { Form: _CompleteOnboardingForm } = completeOnboardingFetcher;
125122
const [searchParams] = useSearchParams();
126123
const origin = searchParams.get("origin");
127124
const fromMarketplaceContext = origin === "marketplace";
@@ -130,7 +127,6 @@ export function VercelOnboardingModal({
130127
() => onboardingData?.availableProjects ?? [],
131128
[onboardingData?.availableProjects]
132129
);
133-
const _hasProjectSelected = onboardingData?.hasProjectSelected ?? false;
134130
const customEnvironments = useMemo(
135131
() => onboardingData?.customEnvironments ?? [],
136132
[onboardingData?.customEnvironments]
@@ -224,10 +220,6 @@ export function VercelOnboardingModal({
224220
environmentId: string;
225221
displayName: string;
226222
} | null>(null);
227-
const _availableEnvSlugsForOnboarding = getAvailableEnvSlugs(
228-
hasStagingEnvironment,
229-
hasPreviewEnvironment
230-
);
231223
const availableEnvSlugsForOnboardingBuildSettings = getAvailableEnvSlugsForBuildSettings(
232224
hasStagingEnvironment,
233225
hasPreviewEnvironment
@@ -375,7 +367,6 @@ export function VercelOnboardingModal({
375367
}
376368
break;
377369

378-
case "installing":
379370
case "project-selection":
380371
case "env-mapping":
381372
case "env-var-sync":
@@ -459,8 +450,6 @@ export function VercelOnboardingModal({
459450

460451
const overlappingEnvVarsCount = enabledEnvVars.filter((v) => existingVars[v.key]).length;
461452

462-
const _isSubmitting = navigation.state === "submitting" || navigation.state === "loading";
463-
464453
const actionUrl = vercelResourcePath(organizationSlug, projectSlug, environmentSlug);
465454

466455
const handleToggleEnvVar = useCallback((key: string, enabled: boolean) => {
@@ -634,19 +623,6 @@ export function VercelOnboardingModal({
634623
gitHubAppInstallations.length,
635624
]);
636625

637-
const _handleFinishOnboarding = useCallback(
638-
(e: React.FormEvent<HTMLFormElement>) => {
639-
e.preventDefault();
640-
const form = e.currentTarget;
641-
const formData = new FormData(form);
642-
completeOnboardingFetcher.submit(formData, {
643-
method: "post",
644-
action: actionUrl,
645-
});
646-
},
647-
[completeOnboardingFetcher, actionUrl]
648-
);
649-
650626
useEffect(() => {
651627
if (
652628
completeOnboardingFetcher.data &&
@@ -698,13 +674,6 @@ export function VercelOnboardingModal({
698674
}
699675
}, [state, onClose, trackOnboarding, isGitHubConnectedForOnboarding]);
700676

701-
useEffect(() => {
702-
if (state === "installing") {
703-
const installUrl = vercelAppInstallPath(organizationSlug, projectSlug);
704-
window.location.href = installUrl;
705-
}
706-
}, [state, organizationSlug, projectSlug]);
707-
708677
useEffect(() => {
709678
if (
710679
envMappingFetcher.data &&
@@ -749,7 +718,6 @@ export function VercelOnboardingModal({
749718
state === "loading-projects" ||
750719
state === "loading-env-mapping" ||
751720
state === "loading-env-vars" ||
752-
state === "installing" ||
753721
(state === "idle" && !onboardingData);
754722

755723
if (isLoadingState) {
@@ -758,9 +726,7 @@ export function VercelOnboardingModal({
758726
open={isOpen}
759727
onOpenChange={(open) => {
760728
if (!open && !fromMarketplaceContext) {
761-
if ((state as string) !== "completed") {
762-
trackOnboarding("vercel onboarding abandoned");
763-
}
729+
trackOnboarding("vercel onboarding abandoned");
764730
onClose();
765731
}
766732
}}
@@ -772,9 +738,30 @@ export function VercelOnboardingModal({
772738
<span>Set up Vercel Integration</span>
773739
</div>
774740
</DialogHeader>
775-
<div className="flex items-center justify-center py-8">
776-
<Spinner color="blue" className="size-6" />
777-
</div>
741+
{onboardingDataUnavailable ? (
742+
<div className="flex flex-col items-start gap-3 py-4">
743+
<Paragraph variant="small">
744+
We couldn't load your Vercel projects. The integration may have been removed or lost
745+
access to this organization on Vercel.
746+
</Paragraph>
747+
<div className="flex items-center gap-2">
748+
{onDataReload && (
749+
<Button variant="secondary/small" onClick={() => onDataReload()}>
750+
Try again
751+
</Button>
752+
)}
753+
{vercelManageAccessUrl && (
754+
<LinkButton to={vercelManageAccessUrl} target="_blank" variant="tertiary/small">
755+
Manage access on Vercel
756+
</LinkButton>
757+
)}
758+
</div>
759+
</div>
760+
) : (
761+
<div className="flex items-center justify-center py-8">
762+
<Spinner color="blue" className="size-6" />
763+
</div>
764+
)}
778765
</DialogContent>
779766
</Dialog>
780767
);

apps/webapp/app/db.server.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,16 @@ export const runOpsSplitReadEnabled: boolean = computeRunOpsSplitReadEnabled({
599599
controlPlaneReplica: $replica,
600600
hasNewUrl: !!env.RUN_OPS_DATABASE_URL,
601601
hasLegacyUrl: !!env.RUN_OPS_LEGACY_DATABASE_URL,
602+
// Observability only: a non-distinct shard handle warns and never changes the gen-1 verdict.
603+
// Empty unless RUN_OPS_SHARDS is configured.
604+
shardHandles: runOpsShardHandles.map((handle) => ({
605+
key: handle.key,
606+
writer: handle.writer,
607+
replica: handle.replica,
608+
// The DECLARED field, not client identity: an aliased shard shares its target's client by
609+
// reference, so identity comparison cannot tell the two apart.
610+
aliasOf: env.RUN_OPS_SHARDS.find((d) => d.key === handle.key)?.aliasOf,
611+
})),
602612
logger,
603613
});
604614

apps/webapp/app/env.server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,10 @@ const EnvironmentSchema = z
940940
DISABLE_HTTP_INSTRUMENTATION: BoolEnv.default(false),
941941

942942
INTERNAL_OTEL_LOG_EXPORTER_URL: z.string().optional(),
943+
944+
// Second trace exporter receiving only `deployment.*` spans; they still flow to the main one
945+
INTERNAL_OTEL_DEPLOYMENT_EVENT_EXPORTER_URL: z.string().optional(),
946+
INTERNAL_OTEL_DEPLOYMENT_EVENT_EXPORTER_AUTH_HEADERS: z.string().optional(),
943947
INTERNAL_OTEL_METRIC_EXPORTER_URL: z.string().optional(),
944948
INTERNAL_OTEL_METRIC_EXPORTER_AUTH_HEADERS: z.string().optional(),
945949
INTERNAL_OTEL_METRIC_EXPORTER_ENABLED: z.string().default("0"),

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

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { BatchTaskRunExecutionResult } from "@trigger.dev/core/v3";
2-
import { ownerEngine } from "@trigger.dev/core/v3/isomorphic";
2+
import { resolveShard, type ShardKey } from "@trigger.dev/core/v3/isomorphic";
33
import {
44
$replica,
55
type PrismaClientOrTransaction,
@@ -13,6 +13,8 @@ import { runStore as defaultRunStore } from "~/v3/runStore.server";
1313
import { BasePresenter } from "./basePresenter.server";
1414

1515
import { boundedIn } from "@trigger.dev/database";
16+
import { runOpsShardReplicas } from "~/v3/runOpsMigration/shardHandles.server";
17+
import { logger } from "~/services/logger.server";
1618
/**
1719
* Run-ops read-through wiring. All optional; absent (or `splitEnabled` falsy) collapses `call` to
1820
* passthrough. `legacyReplica` is a READ REPLICA handle only — there is NO legacy-primary field.
@@ -21,6 +23,8 @@ type ApiBatchResultsReadThroughDeps = {
2123
splitEnabled?: boolean;
2224
newClient?: PrismaReplicaClient;
2325
legacyReplica?: PrismaReplicaClient;
26+
/** Gen-2 shard replicas by shard char; empty (RUN_OPS_SHARDS unset) keeps today's behaviour. */
27+
shardReplicas?: ReadonlyMap<ShardKey, PrismaReplicaClient>;
2428
isPastRetention?: (runId: string) => boolean;
2529
};
2630

@@ -181,16 +185,57 @@ export class ApiBatchResultsPresenter extends BasePresenter {
181185

182186
const taskRunIds = batchRun.items.map((item) => item.taskRunId);
183187

184-
const newRows = (await newClient.taskRun.findMany({
185-
where: { id: { in: boundedIn(taskRunIds) } },
186-
select: memberRunSelect,
187-
})) as TaskRunWithAttempts[];
188+
// A gen-2 id is directly routable to its own shard, so it must not join the gen-1 read:
189+
// it would miss there, and (being dedicated-family) never reach the legacy probe either.
190+
const shardReplicas = this.readThrough?.shardReplicas ?? runOpsShardReplicas;
191+
const genOneIds: string[] = [];
192+
const idsByShard = new Map<ShardKey, string[]>();
193+
for (const id of taskRunIds) {
194+
const shardKey = resolveShard(id);
195+
if (shardKey === "new" || shardKey === "legacy") {
196+
genOneIds.push(id);
197+
} else if (shardReplicas.has(shardKey)) {
198+
const group = idsByShard.get(shardKey);
199+
group ? group.push(id) : idsByShard.set(shardKey, [id]);
200+
} else {
201+
// Not routable and not a gen-1 shape. A gen-1 store is the wrong database, and a
202+
// dedicated-family id never reaches the legacy probe, so falling back there would
203+
// drop the member silently. Drop it loudly instead.
204+
logger.error("ApiBatchResultsPresenter: gen-2 member on an unconfigured shard key", {
205+
runId: id,
206+
shardKey,
207+
configured: [...shardReplicas.keys()],
208+
});
209+
}
210+
}
211+
212+
const newRows = (
213+
genOneIds.length > 0
214+
? ((await newClient.taskRun.findMany({
215+
where: { id: { in: boundedIn(genOneIds) } },
216+
select: memberRunSelect,
217+
})) as TaskRunWithAttempts[])
218+
: []
219+
).concat(
220+
(
221+
await Promise.all(
222+
[...idsByShard.entries()].map(
223+
async ([shardKey, ids]) =>
224+
(await shardReplicas.get(shardKey)!.taskRun.findMany({
225+
where: { id: { in: boundedIn(ids) } },
226+
select: memberRunSelect,
227+
})) as TaskRunWithAttempts[]
228+
)
229+
)
230+
).flat()
231+
);
188232
const runsById = new Map(newRows.map((run) => [run.id, run]));
189233

190-
// A run-ops id can only live on NEW, so only misses that AREN'T run-ops-shaped are candidates
191-
// for the legacy probe — mirrors readThroughRun's per-id "NEW residency skips legacy" rule.
192-
const legacyCandidateIds = taskRunIds.filter(
193-
(id) => !runsById.has(id) && ownerEngine(id) !== "NEW"
234+
// A dedicated-family id (gen-1 v1 or gen-2) can only live on its own store, so only
235+
// misses that AREN'T dedicated-shaped are candidates for the legacy probe — mirrors
236+
// readThroughRun's per-id "dedicated residency skips legacy" rule.
237+
const legacyCandidateIds = genOneIds.filter(
238+
(id) => !runsById.has(id) && resolveShard(id) === "legacy"
194239
);
195240
if (legacyCandidateIds.length > 0) {
196241
const legacyRows = (await legacyReplica.taskRun.findMany({

0 commit comments

Comments
 (0)