1+ import { Redis } from "@upstash/redis" ;
12import { type CoreMessage } from "ai" ;
23import { eq } from "drizzle-orm" ;
34import type { NextRequest } from "next/server" ;
@@ -8,32 +9,20 @@ import { exportSpan, startSessionSpan } from "@/lib/braintrust";
89import { streamChatTurn } from "@/lib/chat-engine" ;
910
1011/**
11- * In-memory cache for session spans .
12- * Maps sessionId -> exported span string
12+ * Upstash Redis client for session span storage .
13+ * Uses REST API which works well with Vercel serverless functions.
1314 *
14- * NOTE: For production with multiple server instances, use Redis or similar:
15- * const redis = new Redis(process.env.REDIS_URL);
16- * await redis.set(`session-span:${sessionId}`, exportedSpan, 'EX', 86400);
17- * const parentSpan = await redis.get(`session-span:${sessionId}`);
15+ * Required env vars (auto-configured by Vercel when you add Upstash):
16+ * - KV_REST_API_URL (or UPSTASH_REDIS_REST_URL)
17+ * - KV_REST_API_TOKEN (or UPSTASH_REDIS_REST_TOKEN)
1818 */
19- const sessionSpanCache = new Map < string , string > ( ) ;
19+ const redis = new Redis ( {
20+ url : process . env . KV_REST_API_URL ! ,
21+ token : process . env . KV_REST_API_TOKEN ! ,
22+ } ) ;
2023
21- // Clean up old sessions periodically (simple TTL implementation)
22- const SESSION_TTL_MS = 24 * 60 * 60 * 1000 ; // 24 hours
23- const sessionTimestamps = new Map < string , number > ( ) ;
24-
25- function cleanupOldSessions ( ) {
26- const now = Date . now ( ) ;
27- for ( const [ sessionId , timestamp ] of sessionTimestamps . entries ( ) ) {
28- if ( now - timestamp > SESSION_TTL_MS ) {
29- sessionSpanCache . delete ( sessionId ) ;
30- sessionTimestamps . delete ( sessionId ) ;
31- }
32- }
33- }
34-
35- // Run cleanup every hour
36- setInterval ( cleanupOldSessions , 60 * 60 * 1000 ) ;
24+ // Session span TTL in seconds (24 hours)
25+ const SESSION_TTL_SECONDS = 24 * 60 * 60 ;
3726
3827export async function POST (
3928 request : NextRequest ,
@@ -61,8 +50,9 @@ export async function POST(
6150 return new Response ( "Widget is disabled" , { status : 403 } ) ;
6251 }
6352
64- // Get or create session-level parent span
65- let parentSpan = sessionSpanCache . get ( sessionId ) ;
53+ // Get or create session-level parent span from Redis
54+ const cacheKey = `session-span:${ sessionId } ` ;
55+ let parentSpan = await redis . get < string > ( cacheKey ) ;
6656
6757 if ( ! parentSpan ) {
6858 // First message in this session - create the root "conversation" span
@@ -77,13 +67,10 @@ export async function POST(
7767 const exported = await exportSpan ( rootSpan ) ;
7868 if ( exported ) {
7969 parentSpan = exported ;
80- sessionSpanCache . set ( sessionId , exported ) ;
81- sessionTimestamps . set ( sessionId , Date . now ( ) ) ;
70+ // Store in Redis with TTL
71+ await redis . set ( cacheKey , exported , { ex : SESSION_TTL_SECONDS } ) ;
8272 }
8373 }
84- } else {
85- // Update timestamp to keep session alive
86- sessionTimestamps . set ( sessionId , Date . now ( ) ) ;
8774 }
8875
8976 const result = await streamChatTurn ( {
0 commit comments