Optional prep
-
Shape Saturday around your KPIs
+
Shape Thursday around your KPIs
Choose your path: spend two minutes with the AI coach or drop the details below so Bradley can tailor the
live walkthrough to your stack.
@@ -499,7 +499,7 @@ function WhatToExpect() {
return (
-
What to Expect on Saturday
+
What to Expect on Thursday
A 60-minute working session that shows you exactly how teams like IAS, Palatine Capital, and Moonstruck Medical shipped
governed AI agents in four weeks.
@@ -646,7 +646,7 @@ function SocialSharing() {
const webinarUrl =
typeof window !== 'undefined' ? `${window.location.origin}/webinar-enterprise-ai` : 'https://theanswer.ai/webinar-enterprise-ai'
const shareText =
- 'Just booked AnswerAI’s “Deploy AI Agents in 4 Weeks” workshop. Live build, governance checklist, and playbooks from IAS & Palatine. Join me:'
+ 'Just booked AnswerAI’s “Deploy AI Agents in weeks” workshop. Live build, governance checklist, and playbooks from IAS & Palatine. Join me:'
const opsMessage = `Hey team — I locked in our seat for AnswerAI's Enterprise AI workshop (${getLocalWebinarDateTime()}). It shows the exact 4-week rollout we want. Grab a spot here: ${webinarUrl}\n\nPS: Add your workflow so they cover it live: ${webinarUrl}#attendee-prep`
const execMessage = `Flagging an Enterprise AI session (${getLocalWebinarDateTime()}) that walks through the 4-week deployment model we’ve been evaluating. Includes ROI benchmarks + compliance templates. Register: ${webinarUrl}`
@@ -775,7 +775,7 @@ function FinalAssurance() {
Share as much (or as little) as you like—your data stays with us
- Everything you submit—whether typed or voiced—is used only to personalize Saturday and the optional pilot
+ Everything you submit—whether typed or voiced—is used only to personalize Thursday and the optional pilot
review. No cold calls, no surprise sequences.
@@ -842,7 +842,7 @@ export default function WebinarThankYou(): JSX.Element {
diff --git a/packages/docs/src/services/trackingService.ts b/packages/docs/src/services/trackingService.ts
index 73c4c05e0f7..282dbff202d 100644
--- a/packages/docs/src/services/trackingService.ts
+++ b/packages/docs/src/services/trackingService.ts
@@ -36,7 +36,6 @@ export class TrackingService {
this.initializeFacebookPixel()
this.initialized = true
- console.log('Tracking services initialized')
}
// Google Analytics 4 setup
@@ -67,7 +66,6 @@ export class TrackingService {
}
})
- console.log('Google Analytics initialized:', gaId)
} catch (error) {
console.error('Google Analytics initialization failed:', error)
}
@@ -103,7 +101,6 @@ export class TrackingService {
;(window.lintrk.q = window.lintrk.q || []).push(args)
}
- console.log('LinkedIn Pixel initialized:', linkedInPixel)
} catch (error) {
console.error('LinkedIn Pixel initialization failed:', error)
}
@@ -134,8 +131,6 @@ export class TrackingService {
window.fbq('init', facebookPixel)
window.fbq('track', 'PageView')
-
- console.log('Facebook Pixel initialized:', facebookPixel)
} catch (error) {
console.error('Facebook Pixel initialization failed:', error)
}
@@ -171,40 +166,46 @@ export class TrackingService {
})
}
- console.log('Event tracked:', eventName, { eventCategory, eventLabel, value })
}
// Track webinar registration conversion
trackWebinarRegistration(data: ConversionData): void {
- const eventData = {
- event: 'webinar_registration',
- eventCategory: 'engagement',
- eventLabel: 'enterprise-ai-webinar',
- value: 1,
- customParameters: {
+ // Google Analytics 4 event tracking
+ if (window.gtag) {
+ window.gtag('event', 'webinar_registration', {
+ event_category: 'engagement',
+ event_label: 'enterprise-ai-webinar',
+ value: 1,
lead_score: data.leadScore,
company: data.company,
job_title: data.jobTitle
- }
+ })
}
- this.trackEvent(eventData)
+ // Meta/Facebook Pixel - CompleteRegistration standard event
+ if (window.fbq) {
+ try {
+ window.fbq('track', 'CompleteRegistration', {
+ content_name: 'Enterprise AI Webinar',
+ value: data.value || 100,
+ currency: 'USD',
+ status: 'confirmed',
+ predicted_ltv: data.leadScore ? data.leadScore * 10 : 100
+ })
+ } catch (error) {
+ console.error('Facebook Pixel tracking failed:', error)
+ }
+ }
- // Enhanced conversion tracking for Google Ads
- if (window.gtag) {
- window.gtag('event', 'conversion', {
- send_to: 'AW-CONVERSION_ID/CONVERSION_LABEL', // Replace with actual conversion ID
- value: data.value || 100, // Estimated lead value
- currency: 'USD',
- transaction_id: `webinar_${Date.now()}`,
- user_data: {
- email_address: data.email,
- first_name: data.firstName,
- last_name: data.lastName,
- job_title: data.jobTitle,
- company: data.company
- }
- })
+ // LinkedIn conversion tracking
+ if (window.lintrk) {
+ try {
+ window.lintrk('track', {
+ conversion_id: 'webinar_registration'
+ })
+ } catch (error) {
+ console.error('LinkedIn tracking failed:', error)
+ }
}
}
@@ -220,9 +221,17 @@ export class TrackingService {
if (window.fbq) {
window.fbq('track', 'PageView')
+
+ // Track ViewContent for webinar pages
+ if (pagePath.includes('webinar')) {
+ window.fbq('track', 'ViewContent', {
+ content_name: pageTitle || 'Enterprise AI Webinar',
+ content_category: 'webinar',
+ content_type: 'product'
+ })
+ }
}
- console.log('Page view tracked:', pagePath)
}
// Track form interactions
@@ -233,6 +242,24 @@ export class TrackingService {
eventLabel: formName,
value: action === 'complete' ? 1 : 0
})
+
+ // Meta Pixel tracking for form interactions
+ if (window.fbq) {
+ try {
+ if (action === 'start') {
+ window.fbq('track', 'Lead', {
+ content_name: formName,
+ content_category: 'webinar'
+ })
+ } else if (action === 'abandon') {
+ window.fbq('trackCustom', 'FormAbandonment', {
+ content_name: formName
+ })
+ }
+ } catch (error) {
+ console.error('Facebook Pixel form tracking failed:', error)
+ }
+ }
}
// Track content engagement
@@ -296,7 +323,6 @@ export class TrackingService {
if (hasUtmParams) {
sessionStorage.setItem('webinar_utm_params', JSON.stringify(utmParams))
- console.log('UTM parameters stored:', utmParams)
}
}
diff --git a/packages/docs/src/theme/Layout/index.tsx b/packages/docs/src/theme/Layout/index.tsx
new file mode 100644
index 00000000000..3506a2ac278
--- /dev/null
+++ b/packages/docs/src/theme/Layout/index.tsx
@@ -0,0 +1,55 @@
+import React, { useMemo } from 'react'
+import OriginalLayout from '@theme-original/Layout'
+import { useLocation } from '@docusaurus/router'
+import styles from './styles.module.css'
+
+export default function Layout(props) {
+ const location = useLocation()
+
+ // Don't show banner on webinar pages
+ const isWebinarPage = location.pathname.includes('webinar')
+
+ // Calculate next Thursday dynamically
+ const nextThursdayText = useMemo(() => {
+ const now = new Date()
+ const result = new Date(now)
+
+ // Get day of week (0 = Sunday, 4 = Thursday)
+ const currentDay = result.getDay()
+ const daysUntilThursday = (4 - currentDay + 7) % 7 || 7
+
+ // Set to next Thursday
+ result.setDate(result.getDate() + daysUntilThursday)
+ result.setHours(11, 0, 0, 0) // 11am local time for display
+
+ // If we're past 11am PT on Thursday, move to next week
+ if (daysUntilThursday === 0 && now > result) {
+ result.setDate(result.getDate() + 7)
+ }
+
+ // Format as "Thursday, Oct 10th"
+ const month = result.toLocaleDateString('en-US', { month: 'short' })
+ const day = result.getDate()
+ const dayWithSuffix =
+ day + (day === 1 || day === 21 || day === 31 ? 'st' : day === 2 || day === 22 ? 'nd' : day === 3 || day === 23 ? 'rd' : 'th')
+
+ return `Thursday, ${month} ${dayWithSuffix}`
+ }, [])
+
+ return (
+
+ {!isWebinarPage && (
+
+
+
🚀 Live Weekly
+
Deploy Enterprise AI in weeks - Workshop {nextThursdayText} at 11am PT
+
+ Register Free →
+
+
+
+ )}
+
+
+ )
+}
diff --git a/packages/docs/src/theme/Layout/styles.module.css b/packages/docs/src/theme/Layout/styles.module.css
new file mode 100644
index 00000000000..e416fe32993
--- /dev/null
+++ b/packages/docs/src/theme/Layout/styles.module.css
@@ -0,0 +1,94 @@
+.webinarBanner {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ width: 100%;
+ background: linear-gradient(90deg, #00303f, #003545);
+ border-bottom: 1px solid rgba(0, 255, 255, 0.3);
+ padding: 0.5rem 1rem;
+ z-index: 9999;
+}
+
+.webinarContent {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.webinarBadge {
+ background: rgba(255, 255, 255, 0.1);
+ color: #00ffff;
+ padding: 0.25rem 0.75rem;
+ border-radius: 20px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ border: 1px solid rgba(0, 255, 255, 0.3);
+}
+
+.webinarText {
+ color: rgba(255, 255, 255, 0.95);
+ font-size: 0.95rem;
+ font-weight: 500;
+}
+
+.webinarCta {
+ background: linear-gradient(135deg, #00ffff, #00cccc);
+ color: #000 !important;
+ padding: 0.4rem 1rem;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ font-weight: 600;
+ text-decoration: none !important;
+ transition: all 0.3s ease;
+ border: 1px solid rgba(0, 255, 255, 0.5);
+}
+
+.webinarCta:hover {
+ background: linear-gradient(135deg, #00ffff, #00ffff);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 255, 255, 0.4);
+ color: #000 !important;
+ text-decoration: none !important;
+}
+
+@media (max-width: 768px) {
+ .webinarBanner {
+ padding: 0.5rem;
+ }
+
+ .webinarContent {
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+
+ .webinarBadge {
+ font-size: 0.8rem;
+ }
+
+ .webinarText {
+ font-size: 0.85rem;
+ text-align: center;
+ }
+
+ .webinarCta {
+ font-size: 0.85rem;
+ }
+}
+
+/* Push everything down to account for the fixed banner */
+:global(body) {
+ padding-top: 54px !important;
+}
+
+/* Remove padding for webinar pages and hide navbar */
+.webinarPage {
+ margin-top: -54px;
+}
+
+.webinarPage :global(.navbar) {
+ display: none !important;
+}
diff --git a/packages/docs/src/types/docusaurus-augmentations.d.ts b/packages/docs/src/types/docusaurus-augmentations.d.ts
new file mode 100644
index 00000000000..496e0bce809
--- /dev/null
+++ b/packages/docs/src/types/docusaurus-augmentations.d.ts
@@ -0,0 +1,12 @@
+declare module '@theme/Layout' {
+ interface Props {
+ readonly title?: string
+ readonly description?: string
+ readonly wrapperClassName?: string
+ readonly pageClassName?: string
+ readonly noFooter?: boolean
+ readonly noNavbar?: boolean
+ readonly searchMetadata?: { readonly version?: string; readonly tag?: string }
+ readonly image?: string
+ }
+}
diff --git a/packages/embed b/packages/embed
index 8581eff48a5..09bc07d7450 160000
--- a/packages/embed
+++ b/packages/embed
@@ -1 +1 @@
-Subproject commit 8581eff48a557ebc142554beebcbf0883351a2c1
+Subproject commit 09bc07d745059581ebfeef12dfcf3e42c918798e
diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts
index 5889b0e1308..0a2fcd1bded 100644
--- a/packages/server/src/Interface.ts
+++ b/packages/server/src/Interface.ts
@@ -127,6 +127,7 @@ export interface IChatMessage {
leadEmail?: string
action?: string | null
followUpPrompts?: string
+ trackingMetadata?: string
}
export interface IChatMessageFeedback {
@@ -330,6 +331,7 @@ export interface IncomingInput {
action?: IAction
chatType?: string
streaming?: boolean
+ trackingMetadata?: ICommonObject
}
export interface IncomingAgentflowInput extends Omit {
diff --git a/packages/server/src/config/passport.ts b/packages/server/src/config/passport.ts
index 88b876a0111..e89eb837454 100644
--- a/packages/server/src/config/passport.ts
+++ b/packages/server/src/config/passport.ts
@@ -96,11 +96,6 @@ export default function (passport: any) {
return done(null, false, { message: 'Authorization code missing' })
}
- const baseUrl = process.env.ATLASSIAN_MCP_SERVER_URL
- if (!baseUrl) {
- return done(null, false, { message: 'ATLASSIAN_MCP_SERVER_URL environment variable is not set' })
- }
-
// Get MCP client info from state parameter (sessionId)
const sessionId = state
const mcpClientInfo = sessionId ? getPendingRegistration(sessionId) : null
@@ -110,7 +105,7 @@ export default function (passport: any) {
}
// Fetch MCP metadata and use MCP client credentials
- const metadata = await fetchMCPMetadata(baseUrl)
+ const metadata = await fetchMCPMetadata()
const tokenURL = metadata.token_endpoint
const clientId = mcpClientInfo.client_id
const clientSecret = mcpClientInfo.client_secret
diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts
index d8573906784..63b10a7d40c 100644
--- a/packages/server/src/database/entities/ChatMessage.ts
+++ b/packages/server/src/database/entities/ChatMessage.ts
@@ -77,4 +77,7 @@ export class ChatMessage implements IChatMessage {
deletedDate: Date
@Column({ nullable: true, type: 'text' })
followUpPrompts?: string
+
+ @Column({ nullable: true, type: 'text', name: 'tracking_metadata' })
+ trackingMetadata?: string
}
diff --git a/packages/server/src/database/migrations/mariadb/1753200000000-AddTrackingMetadataToChatMessage.ts b/packages/server/src/database/migrations/mariadb/1753200000000-AddTrackingMetadataToChatMessage.ts
new file mode 100644
index 00000000000..b9c74d9a89f
--- /dev/null
+++ b/packages/server/src/database/migrations/mariadb/1753200000000-AddTrackingMetadataToChatMessage.ts
@@ -0,0 +1,12 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddTrackingMetadataToChatMessage1753200000000 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ const columnExists = await queryRunner.hasColumn('chat_message', 'tracking_metadata')
+ if (!columnExists) await queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`tracking_metadata\` LONGTEXT;`)
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`tracking_metadata\`;`)
+ }
+}
diff --git a/packages/server/src/database/migrations/mariadb/index.ts b/packages/server/src/database/migrations/mariadb/index.ts
index ee3f80d04c9..286be13097e 100644
--- a/packages/server/src/database/migrations/mariadb/index.ts
+++ b/packages/server/src/database/migrations/mariadb/index.ts
@@ -31,6 +31,7 @@ import { AddTypeToAssistant1733011290987 } from './1733011290987-AddTypeToAssist
import { AddExecutionEntity1738090872625 } from './1738090872625-AddExecutionEntity'
import { AddUserScopingToExecution1738091000000 } from './1738091000000-AddUserScopingToExecution'
import { AddVersioningToChatFlow1753000000000 } from './1753000000000-AddVersioningToChatFlow'
+import { AddTrackingMetadataToChatMessage1753200000000 } from './1753200000000-AddTrackingMetadataToChatMessage'
export const mariadbMigrations = [
Init1693840429259,
@@ -65,5 +66,6 @@ export const mariadbMigrations = [
AddTypeToAssistant1733011290987,
AddExecutionEntity1738090872625,
AddUserScopingToExecution1738091000000,
- AddVersioningToChatFlow1753000000000
+ AddVersioningToChatFlow1753000000000,
+ AddTrackingMetadataToChatMessage1753200000000
]
diff --git a/packages/server/src/database/migrations/mysql/1753200000000-AddTrackingMetadataToChatMessage.ts b/packages/server/src/database/migrations/mysql/1753200000000-AddTrackingMetadataToChatMessage.ts
new file mode 100644
index 00000000000..b9c74d9a89f
--- /dev/null
+++ b/packages/server/src/database/migrations/mysql/1753200000000-AddTrackingMetadataToChatMessage.ts
@@ -0,0 +1,12 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddTrackingMetadataToChatMessage1753200000000 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ const columnExists = await queryRunner.hasColumn('chat_message', 'tracking_metadata')
+ if (!columnExists) await queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`tracking_metadata\` LONGTEXT;`)
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`tracking_metadata\`;`)
+ }
+}
diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts
index a2d625748ed..47f420a20c1 100644
--- a/packages/server/src/database/migrations/mysql/index.ts
+++ b/packages/server/src/database/migrations/mysql/index.ts
@@ -35,6 +35,7 @@ import { AddTypeToAssistant1733011290987 } from './1733011290987-AddTypeToAssist
import { AddExecutionEntity1738090872625 } from './1738090872625-AddExecutionEntity'
import { AddUserScopingToExecution1738091000000 } from './1738091000000-AddUserScopingToExecution'
import { AddVersioningToChatFlow1753000000000 } from './1753000000000-AddVersioningToChatFlow'
+import { AddTrackingMetadataToChatMessage1753200000000 } from './1753200000000-AddTrackingMetadataToChatMessage'
export const mysqlMigrations = [
Init1693840429259,
@@ -73,5 +74,6 @@ export const mysqlMigrations = [
AddTypeToAssistant1733011290987,
AddExecutionEntity1738090872625,
AddUserScopingToExecution1738091000000,
- AddVersioningToChatFlow1753000000000
+ AddVersioningToChatFlow1753000000000,
+ AddTrackingMetadataToChatMessage1753200000000
]
diff --git a/packages/server/src/database/migrations/postgres/1753000000001-AddUniqueConstraintDefaultChatflows.ts b/packages/server/src/database/migrations/postgres/1753000000001-AddUniqueConstraintDefaultChatflows.ts
new file mode 100644
index 00000000000..b124b8a1d25
--- /dev/null
+++ b/packages/server/src/database/migrations/postgres/1753000000001-AddUniqueConstraintDefaultChatflows.ts
@@ -0,0 +1,57 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddUniqueConstraintDefaultChatflows1753000000001 implements MigrationInterface {
+ name = 'AddUniqueConstraintDefaultChatflows1753000000001'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ // NOTE: Before running this migration, you should manually clean up duplicate chatflows
+ // using the SQL scripts provided:
+ // 1. Run 1-analyze-duplicate-chatflows.sql to see duplicates
+ // 2. Run 2-generate-cleanup-statements.sql to get DELETE statements
+ // 3. Manually execute the DELETE statements after reviewing them
+ // 4. Then run this migration to add the unique constraint
+
+ // Check if there are any remaining duplicates before adding the constraint
+ const duplicateCheck = await queryRunner.query(`
+ SELECT COUNT(*) as duplicate_count
+ FROM (
+ SELECT "userId", "parentChatflowId"
+ FROM "chat_flow"
+ WHERE "parentChatflowId" IS NOT NULL
+ AND "deletedDate" IS NULL
+ GROUP BY "userId", "parentChatflowId"
+ HAVING COUNT(*) > 1
+ ) duplicates
+ `)
+
+ const duplicateCount = parseInt(duplicateCheck[0]?.duplicate_count || '0')
+
+ if (duplicateCount > 0) {
+ throw new Error(
+ `Cannot add unique constraint: ${duplicateCount} duplicate chatflow groups still exist. ` +
+ `Please run the manual cleanup scripts first:\n` +
+ `1. 1-analyze-duplicate-chatflows.sql\n` +
+ `2. 2-generate-cleanup-statements.sql\n` +
+ `3. Manually execute the DELETE statements\n` +
+ `4. Then re-run this migration`
+ )
+ }
+
+ // Add the unique constraint
+ // This prevents multiple default chatflows from the same template for the same user
+ await queryRunner.query(`
+ CREATE UNIQUE INDEX "idx_unique_user_parent_chatflow"
+ ON "chat_flow" ("userId", "parentChatflowId")
+ WHERE "parentChatflowId" IS NOT NULL AND "deletedDate" IS NULL
+ `)
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ // Remove the unique constraint
+ // NOTE: If rolling back due to production issues with duplicate chatflows:
+ // 1. First run the duplicate cleanup queries from dbeaver-detailed-analysis.sql
+ // 2. Then roll back this migration
+ // 3. Investigate root cause before re-applying
+ await queryRunner.query(`DROP INDEX IF EXISTS "idx_unique_user_parent_chatflow"`)
+ }
+}
diff --git a/packages/server/src/database/migrations/postgres/1753200000000-AddTrackingMetadataToChatMessage.ts b/packages/server/src/database/migrations/postgres/1753200000000-AddTrackingMetadataToChatMessage.ts
new file mode 100644
index 00000000000..5f37e9d8c7e
--- /dev/null
+++ b/packages/server/src/database/migrations/postgres/1753200000000-AddTrackingMetadataToChatMessage.ts
@@ -0,0 +1,11 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddTrackingMetadataToChatMessage1753200000000 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "tracking_metadata" TEXT;`)
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "tracking_metadata";`)
+ }
+}
diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts
index 3d22b7e531e..2c45294fbb8 100644
--- a/packages/server/src/database/migrations/postgres/index.ts
+++ b/packages/server/src/database/migrations/postgres/index.ts
@@ -61,6 +61,8 @@ import { AddSettingsToCustomTemplate1752780175001 } from './1752780175001-AddSet
import { AddPgvectorExtension1752614575000 } from './1752614575000-AddPgvectorExtension'
import { AddEnabledIntegrationsToOrganization1752614576000 } from './1752614576000-AddEnabledIntegrationsToOrganization'
import { AddVersioningToChatFlow1753000000000 } from './1753000000000-AddVersioningToChatFlow'
+import { AddUniqueConstraintDefaultChatflows1753000000001 } from './1753000000001-AddUniqueConstraintDefaultChatflows'
+import { AddTrackingMetadataToChatMessage1753200000000 } from './1753200000000-AddTrackingMetadataToChatMessage'
export const postgresMigrations = [
Init1693891895163,
@@ -125,5 +127,7 @@ export const postgresMigrations = [
AddSettingsToCustomTemplate1752780175001,
AddPgvectorExtension1752614575000,
AddEnabledIntegrationsToOrganization1752614576000,
- AddVersioningToChatFlow1753000000000
+ AddVersioningToChatFlow1753000000000,
+ AddUniqueConstraintDefaultChatflows1753000000001,
+ AddTrackingMetadataToChatMessage1753200000000
]
diff --git a/packages/server/src/database/migrations/sqlite/1753200000000-AddTrackingMetadataToChatMessage.ts b/packages/server/src/database/migrations/sqlite/1753200000000-AddTrackingMetadataToChatMessage.ts
new file mode 100644
index 00000000000..32c0e086a4f
--- /dev/null
+++ b/packages/server/src/database/migrations/sqlite/1753200000000-AddTrackingMetadataToChatMessage.ts
@@ -0,0 +1,11 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddTrackingMetadataToChatMessage1753200000000 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "tracking_metadata" TEXT;`)
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "tracking_metadata";`)
+ }
+}
diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts
index f055db5d302..66efc4811dd 100644
--- a/packages/server/src/database/migrations/sqlite/index.ts
+++ b/packages/server/src/database/migrations/sqlite/index.ts
@@ -41,6 +41,7 @@ import { AddTypeToAssistant1733011290987 } from './1733011290987-AddTypeToAssist
import { AddExecutionEntity1738090872625 } from './1738090872625-AddExecutionEntity'
import { AddUserScopingToExecution1738091000000 } from './1738091000000-AddUserScopingToExecution'
import { AddVersioningToChatFlow1753000000000 } from './1753000000000-AddVersioningToChatFlow'
+import { AddTrackingMetadataToChatMessage1753200000000 } from './1753200000000-AddTrackingMetadataToChatMessage'
export const sqliteMigrations = [
Init1693835579790,
@@ -85,5 +86,6 @@ export const sqliteMigrations = [
AddTypeToAssistant1733011290987,
AddExecutionEntity1738090872625,
AddUserScopingToExecution1738091000000,
- AddVersioningToChatFlow1753000000000
+ AddVersioningToChatFlow1753000000000,
+ AddTrackingMetadataToChatMessage1753200000000
]
diff --git a/packages/server/src/middlewares/authentication/findOrCreateDefaultChatflowsForUser.ts b/packages/server/src/middlewares/authentication/findOrCreateDefaultChatflowsForUser.ts
index 714f68a9d30..3bfce263679 100644
--- a/packages/server/src/middlewares/authentication/findOrCreateDefaultChatflowsForUser.ts
+++ b/packages/server/src/middlewares/authentication/findOrCreateDefaultChatflowsForUser.ts
@@ -5,9 +5,15 @@ import { User } from '../../database/entities/User'
export const findOrCreateDefaultChatflowsForUser = async (AppDataSource: DataSource, user: User) => {
if (!user) return
- // If user already has a defaultChatflowId, return it
- if (user.defaultChatflowId) {
- return user.defaultChatflowId
+ // Always check database for latest defaultChatflowId to avoid race conditions
+ // Don't rely on the in-memory user object which may be stale
+ const latestUser = await AppDataSource.getRepository(User).findOne({
+ where: { id: user.id },
+ select: ['defaultChatflowId']
+ })
+
+ if (latestUser?.defaultChatflowId) {
+ return latestUser.defaultChatflowId
}
const rawIds = process.env.INITIAL_CHATFLOW_IDS ?? process.env.INITIAL_CHATFLOW_ID ?? ''
@@ -40,7 +46,13 @@ export const findOrCreateDefaultChatflowsForUser = async (AppDataSource: DataSou
// If user already has this chatflow, update their defaultChatflowId if not set
if (existingChatflow) {
- if (!user.defaultChatflowId) {
+ // Check current database state inside transaction to avoid race conditions
+ const currentUser = await userRepo.findOne({
+ where: { id: user.id },
+ select: ['defaultChatflowId']
+ })
+
+ if (!currentUser?.defaultChatflowId) {
await userRepo.update(user.id, { defaultChatflowId: existingChatflow.id })
}
await queryRunner.commitTransaction()
@@ -67,16 +79,68 @@ export const findOrCreateDefaultChatflowsForUser = async (AppDataSource: DataSou
}
// Insert the new chatflow
- const insertResult = await chatFlowRepo.insert(chatflowToImport)
+ // Handle potential unique constraint violation gracefully
+ let insertResult
+ try {
+ insertResult = await chatFlowRepo.insert(chatflowToImport)
+ } catch (error: any) {
+ // If this is a unique constraint violation, another process created the chatflow
+ // Find the existing one and use it
+ if (error.code === '23505' || error.message?.includes('duplicate') || error.message?.includes('unique')) {
+ const existingChatflowAfterConflict = await chatFlowRepo.findOne({
+ where: {
+ userId: user.id,
+ parentChatflowId: firstId
+ }
+ })
+
+ if (existingChatflowAfterConflict) {
+ // Update user's defaultChatflowId if not set
+ const currentUser = await userRepo.findOne({
+ where: { id: user.id },
+ select: ['defaultChatflowId']
+ })
+
+ if (!currentUser?.defaultChatflowId) {
+ await userRepo.update(user.id, { defaultChatflowId: existingChatflowAfterConflict.id })
+ }
+
+ await queryRunner.commitTransaction()
+ await queryRunner.release()
+ return existingChatflowAfterConflict.id
+ }
+ }
+ throw error
+ }
// Get the newly created chatflow ID
const newChatflowId = insertResult.identifiers[0]?.id
if (newChatflowId) {
- // Update the user's defaultChatflowId
- await userRepo.update(user.id, { defaultChatflowId: newChatflowId })
- await queryRunner.commitTransaction()
- return newChatflowId
+ // Double-check that no other process has set a defaultChatflowId in the meantime
+ const finalUserCheck = await userRepo.findOne({
+ where: { id: user.id },
+ select: ['defaultChatflowId']
+ })
+
+ if (!finalUserCheck?.defaultChatflowId) {
+ // Update the user's defaultChatflowId only if still not set
+ await userRepo.update(user.id, { defaultChatflowId: newChatflowId })
+ await queryRunner.commitTransaction()
+ return newChatflowId
+ } else {
+ // Another process already set a defaultChatflowId, use that instead
+ // Soft delete the chatflow we just created to maintain referential integrity
+ const chatflowToDelete = await chatFlowRepo.findOne({ where: { id: newChatflowId } })
+ if (chatflowToDelete) {
+ await chatFlowRepo.update(newChatflowId, {
+ deletedDate: new Date(),
+ name: `${chatflowToDelete.name} (duplicate-removed)`
+ })
+ }
+ await queryRunner.commitTransaction()
+ return finalUserCheck.defaultChatflowId
+ }
}
}
diff --git a/packages/server/src/middlewares/authentication/index.ts b/packages/server/src/middlewares/authentication/index.ts
index 806d4ec85d4..5a723aed917 100644
--- a/packages/server/src/middlewares/authentication/index.ts
+++ b/packages/server/src/middlewares/authentication/index.ts
@@ -158,8 +158,15 @@ export const authenticationHandlerMiddleware =
// Find or create default chatflows for the user
const defaultChatflowId = await findOrCreateDefaultChatflowsForUser(AppDataSource, user)
// Update user with the latest defaultChatflowId
- if (defaultChatflowId) {
- user.defaultChatflowId = defaultChatflowId
+ if (defaultChatflowId && user.defaultChatflowId !== defaultChatflowId) {
+ try {
+ // Persist the update to the database to prevent race conditions
+ await AppDataSource.getRepository(User).update(user.id, { defaultChatflowId })
+ user.defaultChatflowId = defaultChatflowId
+ } catch (error) {
+ console.warn(`Failed to update defaultChatflowId for user ${user.id}:`, error)
+ // Continue - don't break auth flow for non-critical update
+ }
}
// Set permissions based on roles
diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts
index 69fb1b5f976..6fd4a6b08f1 100644
--- a/packages/server/src/utils/buildChatflow.ts
+++ b/packages/server/src/utils/buildChatflow.ts
@@ -564,7 +564,8 @@ export const executeFlow = async ({
fileUploads: uploads ? JSON.stringify(fileUploads) : undefined,
leadEmail: incomingInput.leadEmail,
userId: user?.id ?? agentflow.userId,
- organizationId: user?.organizationId ?? agentflow.organizationId
+ organizationId: user?.organizationId ?? agentflow.organizationId,
+ trackingMetadata: incomingInput.trackingMetadata ? JSON.stringify(incomingInput.trackingMetadata) : undefined
}
await utilAddChatMessage(userMessage, appDataSource)
@@ -702,6 +703,7 @@ export const executeFlow = async ({
prependMessages,
user,
sessionId,
+ trackingMetadata: incomingInput.trackingMetadata,
...(isStreamValid && { sseStreamer, shouldStreamResponse: isStreamValid })
}
@@ -727,7 +729,8 @@ export const executeFlow = async ({
fileUploads: incomingInput.uploads ? JSON.stringify(fileUploads) : undefined,
leadEmail: incomingInput.leadEmail,
userId: user?.id,
- organizationId: user?.organizationId
+ organizationId: user?.organizationId,
+ trackingMetadata: incomingInput.trackingMetadata ? JSON.stringify(incomingInput.trackingMetadata) : undefined
}
await utilAddChatMessage(userMessage, appDataSource)
diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts
index f48e23af06e..f0aeb05e12c 100644
--- a/packages/server/src/utils/index.ts
+++ b/packages/server/src/utils/index.ts
@@ -2067,15 +2067,11 @@ export async function registerOAuthClient(redirectUri: string): Promise<{
authorization_endpoint: string
scope: string
}> {
- const baseUrl = process.env.ATLASSIAN_MCP_SERVER_URL
- if (!baseUrl) {
- throw new Error('ATLASSIAN_MCP_SERVER_URL environment variable is not set')
- }
try {
logger.info('[ATLASSIAN OAUTH]: Starting OAuth client registration')
// Step 1: Get MCP metadata
- const metadata = await fetchMCPMetadata(baseUrl)
+ const metadata = await fetchMCPMetadata()
logger.debug('[ATLASSIAN OAUTH]: Retrieved MCP metadata')
// Step 2: Register dynamic client
@@ -2251,13 +2247,8 @@ export async function refreshStoredCredentialTokens(credentialId: string, appDat
return true // Token is still valid
}
- const baseUrl = process.env.ATLASSIAN_MCP_SERVER_URL
- if (!baseUrl) {
- throw new Error('ATLASSIAN_MCP_SERVER_URL environment variable is not set')
- }
-
// Get MCP metadata
- const metadata = await fetchMCPMetadata(baseUrl)
+ const metadata = await fetchMCPMetadata()
// Refresh tokens
const tokenResponse = await fetch(metadata.token_endpoint, {
diff --git a/packages/server/src/utils/mcp-metadata.ts b/packages/server/src/utils/mcp-metadata.ts
index 87bc3fc7369..7aa170d246b 100644
--- a/packages/server/src/utils/mcp-metadata.ts
+++ b/packages/server/src/utils/mcp-metadata.ts
@@ -3,6 +3,7 @@
*
* Server-side version of MCP metadata fetching with caching
*/
+import { ATLASSIAN_MCP_SERVER_URL } from 'flowise-components'
export interface MCPOAuthMetadata {
issuer: string
@@ -29,8 +30,8 @@ const CACHE_DURATION = 30 * 60 * 1000
* Uses caching to avoid repeated requests
*/
-export async function fetchMCPMetadata(baseUrl: string): Promise {
- const metadataUrl = `${baseUrl}/.well-known/oauth-authorization-server`
+export async function fetchMCPMetadata(): Promise {
+ const metadataUrl = `${ATLASSIAN_MCP_SERVER_URL}/.well-known/oauth-authorization-server`
const cacheKey = metadataUrl
// Check cache first
diff --git a/packages/ui/src/assets/images/jlinc.png b/packages/ui/src/assets/images/jlinc.png
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/ui/src/ui-component/extended/AnalyseFlow.jsx b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx
index 440df07d507..c3b84ad387d 100644
--- a/packages/ui/src/ui-component/extended/AnalyseFlow.jsx
+++ b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx
@@ -25,6 +25,7 @@ import { SwitchInput } from '@/ui-component/switch/Switch'
import { Input } from '@/ui-component/input/Input'
import { StyledButton } from '@/ui-component/button/StyledButton'
import langsmithPNG from '@/assets/images/langchain.png'
+import jlincPNG from '@/assets/images/jlinc.png'
import langfuseSVG from '@/assets/images/langfuse.svg'
import lunarySVG from '@/assets/images/lunary.svg'
import langwatchSVG from '@/assets/images/langwatch.svg'
@@ -40,6 +41,42 @@ import chatflowsApi from '@/api/chatflows'
import Image from 'next/image'
const analyticProviders = [
+ {
+ label: 'JLINC',
+ name: 'jlinc',
+ icon: jlincPNG,
+ url: 'https://jlinc.com',
+ inputs: [
+ {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ credentialNames: ['jlincApi']
+ },
+ {
+ label: 'Agreement ID',
+ name: 'agreementId',
+ type: 'string',
+ optional: true,
+ description: 'The UUID of the agreement that transactions occur under',
+ placeholder: '00000000-0000-0000-0000-000000000000'
+ },
+ {
+ label: 'System Prefix',
+ name: 'systemPrefix',
+ type: 'string',
+ optional: false,
+ description: 'The prefix to give DID user shortnames',
+ placeholder: 'MyProject'
+ },
+ {
+ label: 'On/Off',
+ name: 'status',
+ type: 'boolean',
+ optional: true
+ }
+ ]
+ },
{
label: 'LangSmith',
name: 'langSmith',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cd3dede9758..63e474a6974 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -201,6 +201,9 @@ importers:
aai-embed:
specifier: workspace:*
version: link:../../packages/embed
+ aai-embed-react:
+ specifier: workspace:*
+ version: link:../../packages/embed-react
add:
specifier: ^2.0.6
version: 2.0.6
@@ -503,9 +506,6 @@ importers:
'@textea/json-viewer':
specifier: 2.14.1
version: 2.14.1(@types/react@18.2.15)(immer@9.0.21)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
- aai-embed-react:
- specifier: ~1.0.8
- version: 1.0.8(@types/node@24.3.1)(aai-embed@packages+embed)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1)(typescript@5.5.4)
axios:
specifier: ^1.12.1
version: 1.12.1(debug@4.4.1)
@@ -926,6 +926,9 @@ importers:
'@huggingface/inference':
specifier: ^2.8.1
version: 2.8.1
+ '@jlinc/langchain':
+ specifier: ^0.1.3
+ version: 0.1.3(2mxfj365zf7k5p5gmnphohcam4)
'@langchain/anthropic':
specifier: 0.3.23
version: 0.3.23(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))
@@ -1415,8 +1418,8 @@ importers:
specifier: ^3.0.3
version: 3.0.3
dotenv:
- specifier: ^17.2.2
- version: 17.2.2
+ specifier: ^17.2.3
+ version: 17.2.3
express:
specifier: '>=4.19.2'
version: 5.1.0
@@ -4994,6 +4997,9 @@ packages:
resolution: {integrity: sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ '@jlinc/langchain@0.1.3':
+ resolution: {integrity: sha512-jI3l0auP+MVplglm664vi+9l9GCMHqMg0qX9nYQb5vQHqJC2vTuL6dS2KQCzmJU5W0OdazjAfBxBmruYVN1keQ==}
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -9362,12 +9368,6 @@ packages:
'@zilliz/milvus2-sdk-node@2.6.0':
resolution: {integrity: sha512-0Pblc9WZXWWkEazDFTAHKEi93Bw20ErD1R62/59ZWo2TQcIQM4TP36Tu0x2Y/y+d1j6ot6CpgqfoC5Rub9DSxA==}
- aai-embed-react@1.0.8:
- resolution: {integrity: sha512-lZ/C3oh22nWaDlVEfZd/ayIqRkERPGCbfVEV0uaZSneSOvt50aTjyXMDia3Bvb8WPr/hvFHH5RVtJ+i/BagwYQ==}
- peerDependencies:
- aai-embed: '*'
- react: 18.x
-
abab@2.0.6:
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
deprecated: Use your platform's native atob() and btoa() methods instead
@@ -11845,6 +11845,10 @@ packages:
resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==}
engines: {node: '>=12'}
+ dotenv@17.2.3:
+ resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
+ engines: {node: '>=12'}
+
dpop@2.1.1:
resolution: {integrity: sha512-J0Of2JTiM4h5si0tlbPQ/lkqfZ5wAEVkKYBhkwyyANnPJfWH4VsR5uIkZ+T+OSPIwDYUg1fbd5Mmodd25HjY1w==}
@@ -14982,6 +14986,64 @@ packages:
typeorm:
optional: true
+ langchain@0.3.35:
+ resolution: {integrity: sha512-OkPstP43L3rgaAk72UAVcXy4BzJSiyzXfJsHRBTx9xD3rRtgrAu/jsWpMcsbFAoNO3iGerK+ULzkTzaBJBz6kg==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@langchain/anthropic': '*'
+ '@langchain/aws': '*'
+ '@langchain/cerebras': '*'
+ '@langchain/cohere': '*'
+ '@langchain/core': 0.3.59
+ '@langchain/deepseek': '*'
+ '@langchain/google-genai': '*'
+ '@langchain/google-vertexai': '*'
+ '@langchain/google-vertexai-web': '*'
+ '@langchain/groq': '*'
+ '@langchain/mistralai': '*'
+ '@langchain/ollama': '*'
+ '@langchain/xai': '*'
+ axios: ^1.12.1
+ cheerio: '*'
+ handlebars: ^4.7.8
+ peggy: ^3.0.2
+ typeorm: '*'
+ peerDependenciesMeta:
+ '@langchain/anthropic':
+ optional: true
+ '@langchain/aws':
+ optional: true
+ '@langchain/cerebras':
+ optional: true
+ '@langchain/cohere':
+ optional: true
+ '@langchain/deepseek':
+ optional: true
+ '@langchain/google-genai':
+ optional: true
+ '@langchain/google-vertexai':
+ optional: true
+ '@langchain/google-vertexai-web':
+ optional: true
+ '@langchain/groq':
+ optional: true
+ '@langchain/mistralai':
+ optional: true
+ '@langchain/ollama':
+ optional: true
+ '@langchain/xai':
+ optional: true
+ axios:
+ optional: true
+ cheerio:
+ optional: true
+ handlebars:
+ optional: true
+ peggy:
+ optional: true
+ typeorm:
+ optional: true
+
langchainhub@0.0.11:
resolution: {integrity: sha512-WnKI4g9kU2bHQP136orXr2bcRdgz9iiTBpTN0jWt9IlScUKnJBoD0aa2HOzHURQKeQDnt2JwqVmQ6Depf5uDLQ==}
@@ -26695,6 +26757,36 @@ snapshots:
'@types/yargs': 17.0.33
chalk: 4.1.2
+ '@jlinc/langchain@0.1.3(2mxfj365zf7k5p5gmnphohcam4)':
+ dependencies:
+ axios: 1.12.1(debug@4.4.1)
+ langchain: 0.3.35(idka4flj5daepzmmm4lpdrqkh4)
+ transitivePeerDependencies:
+ - '@langchain/anthropic'
+ - '@langchain/aws'
+ - '@langchain/cerebras'
+ - '@langchain/cohere'
+ - '@langchain/core'
+ - '@langchain/deepseek'
+ - '@langchain/google-genai'
+ - '@langchain/google-vertexai'
+ - '@langchain/google-vertexai-web'
+ - '@langchain/groq'
+ - '@langchain/mistralai'
+ - '@langchain/ollama'
+ - '@langchain/xai'
+ - '@opentelemetry/api'
+ - '@opentelemetry/exporter-trace-otlp-proto'
+ - '@opentelemetry/sdk-trace-base'
+ - cheerio
+ - debug
+ - encoding
+ - handlebars
+ - openai
+ - peggy
+ - typeorm
+ - ws
+
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -26763,11 +26855,6 @@ snapshots:
'@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1)
tslib: 2.8.1
- '@ladle/react-context@1.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
- dependencies:
- react: 18.2.0
- react-dom: 18.2.0(react@18.2.0)
-
'@ladle/react-context@1.0.1(react-dom@18.3.1(react@18.2.0))(react@18.2.0)':
dependencies:
react: 18.2.0
@@ -26823,56 +26910,6 @@ snapshots:
- terser
- typescript
- '@ladle/react@2.5.1(@types/node@24.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1)(typescript@5.5.4)':
- dependencies:
- '@babel/code-frame': 7.27.1
- '@babel/core': 7.28.4
- '@babel/generator': 7.28.3
- '@babel/parser': 7.28.4
- '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.28.4)
- '@babel/preset-env': 7.28.3(@babel/core@7.28.4)
- '@babel/preset-react': 7.18.6(@babel/core@7.28.4)
- '@babel/preset-typescript': 7.21.4(@babel/core@7.28.4)
- '@babel/runtime': 7.28.4
- '@babel/template': 7.27.2
- '@babel/traverse': 7.28.4
- '@babel/types': 7.28.4
- '@ladle/react-context': 1.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
- '@vitejs/plugin-react': 3.1.0(vite@4.5.14(@types/node@24.3.1)(sass@1.92.1))
- axe-core: 4.10.3
- boxen: 7.1.1
- chokidar: 3.6.0
- classnames: 2.5.1
- commander: 9.5.0
- cross-spawn: 7.0.6
- debug: 4.4.1(supports-color@8.1.1)
- default-browser: 3.1.0
- express: 5.1.0
- get-port: 6.1.2
- globby: 13.2.2
- history: 5.3.0
- lodash.merge: 4.6.2
- open: 8.4.2
- prism-react-renderer: 1.3.5(react@18.2.0)
- prop-types: 15.8.1
- query-string: 8.2.0
- react: 18.2.0
- react-dom: 18.2.0(react@18.2.0)
- react-frame-component: 5.2.7(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
- react-inspector: 6.0.2(react@18.2.0)
- vite: 4.5.14(@types/node@24.3.1)(sass@1.92.1)
- vite-tsconfig-paths: 4.3.2(typescript@5.5.4)(vite@4.5.14(@types/node@24.3.1)(sass@1.92.1))
- transitivePeerDependencies:
- - '@types/node'
- - less
- - lightningcss
- - sass
- - stylus
- - sugarss
- - supports-color
- - terser
- - typescript
-
'@langchain/anthropic@0.3.23(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))':
dependencies:
'@anthropic-ai/sdk': 0.52.0
@@ -32237,17 +32274,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitejs/plugin-react@3.1.0(vite@4.5.14(@types/node@24.3.1)(sass@1.92.1))':
- dependencies:
- '@babel/core': 7.28.4
- '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4)
- '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4)
- magic-string: 0.27.0
- react-refresh: 0.14.2
- vite: 4.5.14(@types/node@24.3.1)(sass@1.92.1)
- transitivePeerDependencies:
- - supports-color
-
'@vitejs/plugin-react@4.7.0(vite@7.1.5(@types/node@24.3.1)(jiti@2.5.1)(sass@1.92.1)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.28.4
@@ -32382,23 +32408,6 @@ snapshots:
protobufjs: 7.4.0
winston: 3.17.0
- aai-embed-react@1.0.8(@types/node@24.3.1)(aai-embed@packages+embed)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1)(typescript@5.5.4):
- dependencies:
- '@ladle/react': 2.5.1(@types/node@24.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1)(typescript@5.5.4)
- aai-embed: link:packages/embed
- react: 18.2.0
- transitivePeerDependencies:
- - '@types/node'
- - less
- - lightningcss
- - react-dom
- - sass
- - stylus
- - sugarss
- - supports-color
- - terser
- - typescript
-
abab@2.0.6: {}
abbrev@1.1.1:
@@ -35365,7 +35374,7 @@ snapshots:
dotenv-cli@10.0.0:
dependencies:
cross-spawn: 7.0.6
- dotenv: 17.2.2
+ dotenv: 17.2.3
dotenv-expand: 11.0.7
minimist: 1.2.8
@@ -35381,6 +35390,8 @@ snapshots:
dotenv@17.2.2: {}
+ dotenv@17.2.3: {}
+
dpop@2.1.1: {}
duck@0.1.12:
@@ -39703,6 +39714,43 @@ snapshots:
- openai
- ws
+ langchain@0.3.35(idka4flj5daepzmmm4lpdrqkh4):
+ dependencies:
+ '@langchain/core': 0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67))
+ '@langchain/openai': 0.5.15(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
+ '@langchain/textsplitters': 0.1.0(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))
+ js-tiktoken: 1.0.21
+ js-yaml: 4.1.0
+ jsonpointer: 5.0.1
+ langsmith: 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67))
+ openapi-types: 12.1.3
+ p-retry: 4.6.2
+ uuid: 10.0.0
+ yaml: 2.8.1
+ zod: 3.25.76
+ optionalDependencies:
+ '@langchain/anthropic': 0.3.23(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))
+ '@langchain/aws': 0.1.11(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))
+ '@langchain/cohere': 0.3.4(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(encoding@0.1.13)
+ '@langchain/deepseek': 0.0.2(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
+ '@langchain/google-genai': 0.2.13(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))
+ '@langchain/google-vertexai': 0.2.13(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))
+ '@langchain/groq': 0.2.3(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))(encoding@0.1.13)
+ '@langchain/mistralai': 0.2.1(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))
+ '@langchain/ollama': 0.2.3(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))
+ '@langchain/xai': 0.0.3(@langchain/core@0.3.59(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.67)))(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))
+ axios: 1.12.1(debug@4.4.1)
+ cheerio: 1.1.2
+ handlebars: 4.7.8
+ typeorm: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@24.3.1)(typescript@5.5.4))
+ transitivePeerDependencies:
+ - '@opentelemetry/api'
+ - '@opentelemetry/exporter-trace-otlp-proto'
+ - '@opentelemetry/sdk-trace-base'
+ - encoding
+ - openai
+ - ws
+
langchainhub@0.0.11: {}
langfuse-core@3.37.6:
@@ -44138,12 +44186,6 @@ snapshots:
react-fast-compare@3.2.2: {}
- react-frame-component@5.2.7(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
- dependencies:
- prop-types: 15.8.1
- react: 18.2.0
- react-dom: 18.2.0(react@18.2.0)
-
react-frame-component@5.2.7(prop-types@15.8.1)(react-dom@18.3.1(react@18.2.0))(react@18.2.0):
dependencies:
prop-types: 15.8.1
@@ -47605,17 +47647,6 @@ snapshots:
- supports-color
- typescript
- vite-tsconfig-paths@4.3.2(typescript@5.5.4)(vite@4.5.14(@types/node@24.3.1)(sass@1.92.1)):
- dependencies:
- debug: 4.4.1(supports-color@8.1.1)
- globrex: 0.1.2
- tsconfck: 3.1.6(typescript@5.5.4)
- optionalDependencies:
- vite: 4.5.14(@types/node@24.3.1)(sass@1.92.1)
- transitivePeerDependencies:
- - supports-color
- - typescript
-
vite@4.5.14(@types/node@18.15.11)(sass@1.92.1)(terser@5.44.0):
dependencies:
esbuild: 0.25.9
@@ -47627,16 +47658,6 @@ snapshots:
sass: 1.92.1
terser: 5.44.0
- vite@4.5.14(@types/node@24.3.1)(sass@1.92.1):
- dependencies:
- esbuild: 0.25.9
- postcss: 8.5.6
- rollup: 3.29.5
- optionalDependencies:
- '@types/node': 24.3.1
- fsevents: 2.3.3
- sass: 1.92.1
-
vite@7.1.5(@types/node@24.3.1)(jiti@2.5.1)(sass@1.92.1)(terser@5.44.0)(yaml@2.8.1):
dependencies:
esbuild: 0.25.9
diff --git a/render.yaml b/render.yaml
index 6233b7e16c5..c8df4fbfc77 100644
--- a/render.yaml
+++ b/render.yaml
@@ -330,7 +330,7 @@ services:
# ================================================================
- type: keyvalue
name: aai-unified4-redis
- plan: standard
+ plan: free
region: virginia
maxmemoryPolicy: allkeys-lru
ipAllowList: []
diff --git a/scripts/dbeaver-detailed-analysis.sql b/scripts/dbeaver-detailed-analysis.sql
new file mode 100644
index 00000000000..70d5fbbcd86
--- /dev/null
+++ b/scripts/dbeaver-detailed-analysis.sql
@@ -0,0 +1,40 @@
+-- DETAILED ANALYSIS TO UNDERSTAND THE MISMATCH
+-- Run each query separately to understand the different scenarios
+
+-- Query 1: Show ALL duplicate scenarios with detailed breakdown
+SELECT
+ u.email,
+ u."defaultChatflowId" as user_default,
+ cf."parentChatflowId" as template_id,
+ cf.id as chatflow_id,
+ cf.name as chatflow_name,
+ cf."createdDate",
+ CASE WHEN cf.id::uuid = u."defaultChatflowId"::uuid THEN 'YES - USER DEFAULT' ELSE 'No' END as is_current_default,
+ ROW_NUMBER() OVER (PARTITION BY cf."userId", cf."parentChatflowId" ORDER BY cf."createdDate") as age_rank,
+ COUNT(*) OVER (PARTITION BY cf."userId", cf."parentChatflowId") as total_duplicates,
+ CASE
+ WHEN cf.id::uuid = u."defaultChatflowId"::uuid THEN 'KEEP - User Default'
+ WHEN cf.id::uuid != u."defaultChatflowId"::uuid AND
+ EXISTS (SELECT 1 FROM "chat_flow" cf2 WHERE cf2."userId" = cf."userId"
+ AND cf2."parentChatflowId" = cf."parentChatflowId"
+ AND cf2.id::uuid = u."defaultChatflowId"::uuid
+ AND cf2."deletedDate" IS NULL) THEN 'DELETE - User default exists elsewhere'
+ WHEN NOT EXISTS (SELECT 1 FROM "chat_flow" cf2 WHERE cf2."userId" = cf."userId"
+ AND cf2."parentChatflowId" = cf."parentChatflowId"
+ AND cf2.id::uuid = u."defaultChatflowId"::uuid
+ AND cf2."deletedDate" IS NULL) THEN 'COMPLEX - User default not in this group'
+ ELSE 'UNKNOWN'
+ END as recommended_action
+FROM "chat_flow" cf
+JOIN "user" u ON cf."userId" = u.id
+WHERE cf."parentChatflowId" IS NOT NULL
+ AND cf."deletedDate" IS NULL
+ AND (cf."userId", cf."parentChatflowId") IN (
+ SELECT "userId", "parentChatflowId"
+ FROM "chat_flow"
+ WHERE "parentChatflowId" IS NOT NULL
+ AND "deletedDate" IS NULL
+ GROUP BY "userId", "parentChatflowId"
+ HAVING COUNT(*) > 1
+ )
+ORDER BY u.email, cf."parentChatflowId", cf."createdDate";
diff --git a/scripts/dbeaver-improved-deletes.sql b/scripts/dbeaver-improved-deletes.sql
new file mode 100644
index 00000000000..3c2bb6b4568
--- /dev/null
+++ b/scripts/dbeaver-improved-deletes.sql
@@ -0,0 +1,76 @@
+-- IMPROVED DELETE GENERATION
+-- Run each query separately to avoid UNION syntax issues
+
+-- Query 1: Generate DELETEs for straightforward cases (user default exists in group)
+SELECT
+ 'SAFE DELETE' as category,
+ u.email,
+ cf.id as chatflow_to_delete,
+ cf.name as chatflow_name,
+ cf."createdDate",
+ cf."parentChatflowId",
+ 'Safe - User default exists in this group' as reason,
+ 'DELETE FROM "chat_flow" WHERE id = ''' || cf.id || ''';' as delete_statement
+FROM "chat_flow" cf
+JOIN "user" u ON cf."userId" = u.id
+WHERE cf."parentChatflowId" IS NOT NULL
+ AND cf."deletedDate" IS NULL
+ AND cf.id::uuid != u."defaultChatflowId"::uuid -- Not the user's default
+ AND (cf."userId", cf."parentChatflowId") IN (
+ SELECT "userId", "parentChatflowId"
+ FROM "chat_flow"
+ WHERE "parentChatflowId" IS NOT NULL
+ AND "deletedDate" IS NULL
+ GROUP BY "userId", "parentChatflowId"
+ HAVING COUNT(*) > 1
+ )
+ AND EXISTS (
+ -- User's default exists in this duplicate group
+ SELECT 1 FROM "chat_flow" cf2
+ WHERE cf2."userId" = cf."userId"
+ AND cf2."parentChatflowId" = cf."parentChatflowId"
+ AND cf2.id::uuid = u."defaultChatflowId"::uuid
+ AND cf2."deletedDate" IS NULL
+ )
+ORDER BY email, "parentChatflowId", "createdDate";
+
+-- Query 2: Generate DELETEs for complex cases (keep oldest when user default is elsewhere)
+-- RUN THIS SEPARATELY
+SELECT
+ 'COMPLEX DELETE' as category,
+ u.email,
+ cf.id as chatflow_to_delete,
+ cf.name as chatflow_name,
+ cf."createdDate",
+ cf."parentChatflowId",
+ 'Keep oldest - User default is for different template' as reason,
+ 'DELETE FROM "chat_flow" WHERE id = ''' || cf.id || ''';' as delete_statement
+FROM "chat_flow" cf
+JOIN "user" u ON cf."userId" = u.id
+WHERE cf."parentChatflowId" IS NOT NULL
+ AND cf."deletedDate" IS NULL
+ AND (cf."userId", cf."parentChatflowId") IN (
+ SELECT "userId", "parentChatflowId"
+ FROM "chat_flow"
+ WHERE "parentChatflowId" IS NOT NULL
+ AND "deletedDate" IS NULL
+ GROUP BY "userId", "parentChatflowId"
+ HAVING COUNT(*) > 1
+ )
+ AND NOT EXISTS (
+ -- User's default does NOT exist in this duplicate group
+ SELECT 1 FROM "chat_flow" cf2
+ WHERE cf2."userId" = cf."userId"
+ AND cf2."parentChatflowId" = cf."parentChatflowId"
+ AND cf2.id::uuid = u."defaultChatflowId"::uuid
+ AND cf2."deletedDate" IS NULL
+ )
+ AND cf."createdDate" > (
+ -- Only delete if this is NOT the oldest in the group
+ SELECT MIN("createdDate")
+ FROM "chat_flow" cf3
+ WHERE cf3."userId" = cf."userId"
+ AND cf3."parentChatflowId" = cf."parentChatflowId"
+ AND cf3."deletedDate" IS NULL
+ )
+ORDER BY email, "parentChatflowId", "createdDate";
diff --git a/scripts/test-race-condition.sh b/scripts/test-race-condition.sh
new file mode 100755
index 00000000000..6e66b0febc9
--- /dev/null
+++ b/scripts/test-race-condition.sh
@@ -0,0 +1,123 @@
+#!/bin/bash
+
+# Race Condition Test Script (Bash version)
+# This script sends multiple concurrent requests to test the default chatflow creation race condition
+
+# Configuration
+BASE_URL="${API_BASE_URL:-http://localhost:4000}"
+AUTH_TOKEN="${TEST_AUTH_TOKEN:-}"
+CONCURRENT_REQUESTS=5
+
+if [ -z "$AUTH_TOKEN" ]; then
+ echo "❌ Please provide TEST_AUTH_TOKEN environment variable"
+ echo "Usage: TEST_AUTH_TOKEN=\"your-jwt-token\" ./test-race-condition.sh"
+ exit 1
+fi
+
+echo "🏁 Starting Race Condition Test"
+echo " Concurrent requests: $CONCURRENT_REQUESTS"
+echo " Target URL: $BASE_URL/api/v1/auth/me"
+echo ""
+
+# Function to make a single request
+make_request() {
+ local request_id=$1
+ local start_time=$(date +%s)
+
+ local response=$(curl -s -w "\n%{http_code}\n%{time_total}" \
+ -H "Authorization: Bearer $AUTH_TOKEN" \
+ -H "Content-Type: application/json" \
+ "$BASE_URL/api/v1/auth/me" 2>/dev/null)
+
+ local end_time=$(date +%s)
+ local duration=$((end_time - start_time))
+
+ # Parse response
+ local body=$(echo "$response" | head -n 1)
+ local http_code=$(echo "$response" | tail -n 2 | head -n 1)
+ local time_total=$(echo "$response" | tail -n 1)
+
+ if [ "$http_code" = "200" ]; then
+ # Extract defaultChatflowId using basic text processing
+ local chatflow_id=$(echo "$body" | grep -o '"defaultChatflowId":"[^"]*"' | cut -d'"' -f4)
+ if [ -z "$chatflow_id" ] || [ "$chatflow_id" = "null" ]; then
+ chatflow_id="null"
+ fi
+
+ echo "✅ Request $request_id: Success (${duration}ms) - Chatflow: $chatflow_id"
+ echo "$request_id:$chatflow_id" >> /tmp/race_test_results.txt
+ else
+ echo "❌ Request $request_id: Failed ($http_code) (${duration}ms)"
+ echo "$request_id:ERROR" >> /tmp/race_test_results.txt
+ fi
+}
+
+# Clear previous results
+rm -f /tmp/race_test_results.txt
+
+# Launch concurrent requests
+echo "🚀 Launching $CONCURRENT_REQUESTS concurrent requests..."
+for i in $(seq 1 $CONCURRENT_REQUESTS); do
+ make_request $i &
+done
+
+# Wait for all background jobs to complete
+wait
+
+echo ""
+echo "📊 Test Results:"
+echo "================"
+
+# Analyze results
+if [ -f /tmp/race_test_results.txt ]; then
+ total_requests=$(wc -l < /tmp/race_test_results.txt)
+ successful_requests=$(grep -v "ERROR" /tmp/race_test_results.txt | wc -l)
+ failed_requests=$(grep "ERROR" /tmp/race_test_results.txt | wc -l)
+
+ echo "✅ Successful requests: $successful_requests/$total_requests"
+ echo "❌ Failed requests: $failed_requests/$total_requests"
+
+ if [ $successful_requests -gt 0 ]; then
+ # Extract unique chatflow IDs (excluding null and ERROR)
+ unique_chatflows=$(grep -v "ERROR" /tmp/race_test_results.txt | cut -d':' -f2 | grep -v "null" | sort -u | wc -l)
+
+ echo ""
+ echo "🔍 Race Condition Analysis:"
+ echo " Unique default chatflow IDs created: $unique_chatflows"
+
+ if [ $unique_chatflows -gt 1 ]; then
+ echo "⚠️ RACE CONDITION DETECTED! Multiple chatflows created:"
+ grep -v "ERROR" /tmp/race_test_results.txt | cut -d':' -f2 | grep -v "null" | sort -u | while read chatflow_id; do
+ echo " Chatflow ID: $chatflow_id"
+ done
+ elif [ $unique_chatflows -eq 1 ]; then
+ echo "✅ No race condition detected - only one chatflow created"
+ chatflow_id=$(grep -v "ERROR" /tmp/race_test_results.txt | cut -d':' -f2 | grep -v "null" | head -n 1)
+ echo " Chatflow ID: $chatflow_id"
+ else
+ echo "⚠️ No default chatflows were created"
+ fi
+
+ echo ""
+ echo "📝 Individual Request Results:"
+ while IFS=':' read -r request_id chatflow_id; do
+ if [ "$chatflow_id" = "ERROR" ]; then
+ echo " ❌ Request $request_id: Failed"
+ else
+ echo " ✅ Request $request_id: $chatflow_id"
+ fi
+ done < /tmp/race_test_results.txt
+ fi
+fi
+
+# Cleanup
+rm -f /tmp/race_test_results.txt
+
+echo ""
+echo "💾 To verify in database, run:"
+echo "SELECT cf.id, cf.\"userId\", cf.\"parentChatflowId\", cf.\"createdDate\""
+echo "FROM \"chatflow\" cf"
+echo "JOIN \"user\" u ON cf.\"userId\" = u.id"
+echo "WHERE u.auth0_id = 'YOUR_AUTH0_ID'"
+echo " AND cf.\"parentChatflowId\" IS NOT NULL"
+echo "ORDER BY cf.\"createdDate\" DESC;"