From 1618ae5c668b3bb43249bb187d123a907dffee9d Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 8 Jun 2026 14:14:41 -0400 Subject: [PATCH 1/3] fix(web): gate platform API calls on organization ID readiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Platform API endpoints require the Vellum-Organization-Id header. During app startup, the organization store loads asynchronously, but the assistant lifecycle query and client feature flag fetch could fire before it resolved — sending requests without the header and getting 400 responses from Django. This caused the hatching screen to hang on "Setting up your assistant…" indefinitely. Gate both the lifecycle server query and the feature flag sync on the organization ID being available before firing. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/src/assistant/use-lifecycle.ts | 10 +++++++--- apps/web/src/root-layout.tsx | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/web/src/assistant/use-lifecycle.ts b/apps/web/src/assistant/use-lifecycle.ts index 57a91cdfbb7..0f80ad84878 100644 --- a/apps/web/src/assistant/use-lifecycle.ts +++ b/apps/web/src/assistant/use-lifecycle.ts @@ -52,13 +52,19 @@ export function useAssistantLifecycle({ }: UseAssistantLifecycleOptions): void { const queryClient = useQueryClient(); + const currentOrganizationId = + useOrganizationStore.use.currentOrganizationId(); + // Whether to query the server-side status at all. Gateway-auth // mode and "local mode without platform session" short-circuit // to local states without ever calling /assistant/. + // Platform API calls require the Vellum-Organization-Id header; + // wait for the org store to resolve before firing them. const shouldQueryServer = isAuthenticated(sessionStatus) && !isGatewayAuthMode() && - (hasPlatformSession || !isLocalMode()); + (hasPlatformSession || !isLocalMode()) && + !!currentOrganizationId; // Which platform assistant the user has selected, gated by the // multi-platform-assistant flag. When the flag is off (or no @@ -67,8 +73,6 @@ export function useAssistantLifecycle({ // pre-multi-assistant behavior. const multiAssistantEnabled = useClientFeatureFlagStore.use.multiPlatformAssistant(); - const currentOrganizationId = - useOrganizationStore.use.currentOrganizationId(); const byOrg = useResolvedAssistantsStore.use.selectedPlatformAssistantByOrg(); const selectedPlatformAssistantId = diff --git a/apps/web/src/root-layout.tsx b/apps/web/src/root-layout.tsx index e2f22dd4620..e1ee35e4e99 100644 --- a/apps/web/src/root-layout.tsx +++ b/apps/web/src/root-layout.tsx @@ -41,6 +41,7 @@ import { useElectronFeatureFlagBridge } from "@/runtime/electron-feature-flags"; import { TimezoneSync } from "@/components/timezone-sync"; import { retireAssistant } from "@/assistant/retire-service"; import { selectPlatformAssistant } from "@/assistant/select-platform-assistant"; +import { useOrganizationStore } from "@/stores/organization-store"; import { CreateAssistantDialog } from "@/components/create-assistant-dialog"; import { ConfirmDialog } from "@vellumai/design-library/components/confirm-dialog"; import { toast } from "@vellumai/design-library/components/toast"; @@ -90,7 +91,8 @@ export function RootLayout() { const isSessionInitializing = useIsSessionInitializing(); const hasPlatformSession = useHasPlatformSession(); const isNonProduction = useEnvironmentStore.use.isNonProduction(); - useClientFeatureFlagSync(hasPlatformSession && !isSessionInitializing); + const hasOrganization = !!useOrganizationStore.use.currentOrganizationId(); + useClientFeatureFlagSync(hasPlatformSession && !isSessionInitializing && hasOrganization); useAssistantLifecycle({ sessionStatus, isRetired: false, From 471d9bd4790dac588eea095c7dcd7fe2cbfedc5d Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 8 Jun 2026 14:21:03 -0400 Subject: [PATCH 2/3] fix(web): also gate imperative checkAssistant on org readiness The Codex review correctly identified that respondToInputs() calls checkAssistant() imperatively via fetchQuery, bypassing the passive useAssistantQuery enabled gate. Pass hasOrganization through the service inputs and short-circuit respondToInputs() before the imperative fetch when the org store hasn't resolved yet. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/src/assistant/lifecycle-service.ts | 7 +++++++ apps/web/src/assistant/use-lifecycle.ts | 2 ++ 2 files changed, 9 insertions(+) diff --git a/apps/web/src/assistant/lifecycle-service.ts b/apps/web/src/assistant/lifecycle-service.ts index d639f982b4f..468bcee85d4 100644 --- a/apps/web/src/assistant/lifecycle-service.ts +++ b/apps/web/src/assistant/lifecycle-service.ts @@ -83,6 +83,12 @@ export interface LifecycleServiceInputs { * behavior. Optional so existing `setInputs` callers/tests need no change. */ selectedPlatformAssistantId?: string | null; + /** + * Whether the organization store has resolved an active org ID. + * Platform API calls require the Vellum-Organization-Id header; + * `respondToInputs` defers `checkAssistant` until this is true. + */ + hasOrganization?: boolean; } const NOOP_REDIRECT = (_: string) => {}; @@ -194,6 +200,7 @@ class AssistantLifecycleService { if (this.inputs.hasPlatformSession) { setSelfHostedConnection(null); } + if (!this.inputs.hasOrganization) return; await this.checkAssistant(); } diff --git a/apps/web/src/assistant/use-lifecycle.ts b/apps/web/src/assistant/use-lifecycle.ts index 0f80ad84878..b33ba73bc69 100644 --- a/apps/web/src/assistant/use-lifecycle.ts +++ b/apps/web/src/assistant/use-lifecycle.ts @@ -101,6 +101,7 @@ export function useAssistantLifecycle({ resolveOnboardingRedirect, queryClient, selectedPlatformAssistantId, + hasOrganization: !!currentOrganizationId, }); void lifecycleService.respondToInputs(); }, [ @@ -112,6 +113,7 @@ export function useAssistantLifecycle({ resolveOnboardingRedirect, queryClient, selectedPlatformAssistantId, + currentOrganizationId, ]); // Hand poll results to the service — it decides whether to From ed61d59b9991aad7f5922ad751c807cf8748b8a6 Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 8 Jun 2026 14:27:39 -0400 Subject: [PATCH 3/3] refactor(web): use canonical useIsOrgReady() hook for org gate Replace raw `!!currentOrganizationId` / `hasOrganization` checks with the canonical `useIsOrgReady()` hook (established in #32912). The hook returns `!hasPlatformSession || currentOrgId != null`, so local/gateway sessions pass through instead of being blocked on an org that never arrives. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/src/assistant/lifecycle-service.ts | 7 ++++--- apps/web/src/assistant/use-lifecycle.ts | 8 +++++--- apps/web/src/root-layout.tsx | 6 +++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/web/src/assistant/lifecycle-service.ts b/apps/web/src/assistant/lifecycle-service.ts index 468bcee85d4..1537351651d 100644 --- a/apps/web/src/assistant/lifecycle-service.ts +++ b/apps/web/src/assistant/lifecycle-service.ts @@ -84,11 +84,12 @@ export interface LifecycleServiceInputs { */ selectedPlatformAssistantId?: string | null; /** - * Whether the organization store has resolved an active org ID. + * Whether the org store has hydrated (or no platform session exists). * Platform API calls require the Vellum-Organization-Id header; * `respondToInputs` defers `checkAssistant` until this is true. + * Mirrors `useIsOrgReady()` from the React layer. */ - hasOrganization?: boolean; + isOrgReady?: boolean; } const NOOP_REDIRECT = (_: string) => {}; @@ -200,7 +201,7 @@ class AssistantLifecycleService { if (this.inputs.hasPlatformSession) { setSelfHostedConnection(null); } - if (!this.inputs.hasOrganization) return; + if (!this.inputs.isOrgReady) return; await this.checkAssistant(); } diff --git a/apps/web/src/assistant/use-lifecycle.ts b/apps/web/src/assistant/use-lifecycle.ts index b33ba73bc69..93ef900f648 100644 --- a/apps/web/src/assistant/use-lifecycle.ts +++ b/apps/web/src/assistant/use-lifecycle.ts @@ -19,6 +19,7 @@ import { lifecycleService } from "@/assistant/lifecycle-service"; import { useAssistantQuery } from "@/assistant/queries"; import { isGatewayAuthMode } from "@/lib/auth/gateway-session"; import { isLocalMode } from "@/lib/local-mode"; +import { useIsOrgReady } from "@/hooks/use-is-org-ready"; import { isAuthenticated, type SessionStatus } from "@/stores/session-status"; import { useClientFeatureFlagStore } from "@/stores/client-feature-flag-store"; import { useResolvedAssistantsStore } from "@/stores/resolved-assistants-store"; @@ -52,6 +53,7 @@ export function useAssistantLifecycle({ }: UseAssistantLifecycleOptions): void { const queryClient = useQueryClient(); + const isOrgReady = useIsOrgReady(); const currentOrganizationId = useOrganizationStore.use.currentOrganizationId(); @@ -64,7 +66,7 @@ export function useAssistantLifecycle({ isAuthenticated(sessionStatus) && !isGatewayAuthMode() && (hasPlatformSession || !isLocalMode()) && - !!currentOrganizationId; + isOrgReady; // Which platform assistant the user has selected, gated by the // multi-platform-assistant flag. When the flag is off (or no @@ -101,7 +103,7 @@ export function useAssistantLifecycle({ resolveOnboardingRedirect, queryClient, selectedPlatformAssistantId, - hasOrganization: !!currentOrganizationId, + isOrgReady, }); void lifecycleService.respondToInputs(); }, [ @@ -113,7 +115,7 @@ export function useAssistantLifecycle({ resolveOnboardingRedirect, queryClient, selectedPlatformAssistantId, - currentOrganizationId, + isOrgReady, ]); // Hand poll results to the service — it decides whether to diff --git a/apps/web/src/root-layout.tsx b/apps/web/src/root-layout.tsx index e1ee35e4e99..1bb21fc6b76 100644 --- a/apps/web/src/root-layout.tsx +++ b/apps/web/src/root-layout.tsx @@ -41,7 +41,7 @@ import { useElectronFeatureFlagBridge } from "@/runtime/electron-feature-flags"; import { TimezoneSync } from "@/components/timezone-sync"; import { retireAssistant } from "@/assistant/retire-service"; import { selectPlatformAssistant } from "@/assistant/select-platform-assistant"; -import { useOrganizationStore } from "@/stores/organization-store"; +import { useIsOrgReady } from "@/hooks/use-is-org-ready"; import { CreateAssistantDialog } from "@/components/create-assistant-dialog"; import { ConfirmDialog } from "@vellumai/design-library/components/confirm-dialog"; import { toast } from "@vellumai/design-library/components/toast"; @@ -91,8 +91,8 @@ export function RootLayout() { const isSessionInitializing = useIsSessionInitializing(); const hasPlatformSession = useHasPlatformSession(); const isNonProduction = useEnvironmentStore.use.isNonProduction(); - const hasOrganization = !!useOrganizationStore.use.currentOrganizationId(); - useClientFeatureFlagSync(hasPlatformSession && !isSessionInitializing && hasOrganization); + const isOrgReady = useIsOrgReady(); + useClientFeatureFlagSync(hasPlatformSession && !isSessionInitializing && isOrgReady); useAssistantLifecycle({ sessionStatus, isRetired: false,