import { db } from '@sim/db' import { memory } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, eq, isNull, like } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createMemoryContract, deleteMemoryByQueryContract, listMemoriesContract, memoryMessageSchema, } from '@/lib/api/contracts/memory' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('MemoryAPI') export const dynamic = 'force-dynamic' export const runtime = 'nodejs' export const GET = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const authResult = await checkInternalAuth(request) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized memory access attempt`) return NextResponse.json( { success: false, error: { message: authResult.error || 'Authentication required' } }, { status: 401 } ) } const validation = await parseRequest(listMemoriesContract, request, {}) if (!validation.success) return validation.response const { workspaceId, query: searchQuery, limit } = validation.data.query if (!workspaceId) { return NextResponse.json( { success: false, error: { message: 'workspaceId parameter is required' } }, { status: 400 } ) } const access = await checkWorkspaceAccess(workspaceId, authResult.userId) if (!access.exists) { return NextResponse.json( { success: false, error: { message: 'Workspace not found' } }, { status: 404 } ) } if (!access.hasAccess) { return NextResponse.json( { success: false, error: { message: 'Access denied to this workspace' } }, { status: 403 } ) } const conditions = [isNull(memory.deletedAt), eq(memory.workspaceId, workspaceId)] if (searchQuery) { conditions.push(like(memory.key, `%${searchQuery}%`)) } const rawMemories = await db .select() .from(memory) .where(and(...conditions)) .orderBy(memory.createdAt) .limit(limit) const enrichedMemories = rawMemories.map((mem) => ({ conversationId: mem.key, data: mem.data, })) logger.info( `[${requestId}] Found ${enrichedMemories.length} memories for workspace: ${workspaceId}` ) return NextResponse.json( { success: true, data: { memories: enrichedMemories } }, { status: 200 } ) } catch (error: any) { logger.error(`[${requestId}] Error searching memories`, { error }) return NextResponse.json( { success: false, error: { message: error.message || 'Failed to search memories' } }, { status: 500 } ) } }) export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const authResult = await checkInternalAuth(request) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized memory creation attempt`) return NextResponse.json( { success: false, error: { message: authResult.error || 'Authentication required' } }, { status: 401 } ) } const validation = await parseRequest(createMemoryContract, request, {}) if (!validation.success) return validation.response const { key, data, workspaceId } = validation.data.body if (!key) { return NextResponse.json( { success: false, error: { message: 'Memory key is required' } }, { status: 400 } ) } if (!data) { return NextResponse.json( { success: false, error: { message: 'Memory data is required' } }, { status: 400 } ) } if (!workspaceId) { return NextResponse.json( { success: false, error: { message: 'workspaceId is required' } }, { status: 400 } ) } const access = await checkWorkspaceAccess(workspaceId, authResult.userId) if (!access.exists) { return NextResponse.json( { success: false, error: { message: 'Workspace not found' } }, { status: 404 } ) } if (!access.hasAccess) { return NextResponse.json( { success: false, error: { message: 'Access denied to this workspace' } }, { status: 403 } ) } if (!access.canWrite) { return NextResponse.json( { success: false, error: { message: 'Write access denied to this workspace' } }, { status: 403 } ) } const dataToValidate = Array.isArray(data) ? data : [data] for (const msg of dataToValidate) { const parsedMessage = memoryMessageSchema.safeParse(msg) if (!parsedMessage.success) { const role = msg && typeof msg === 'object' && 'role' in msg ? (msg as { role?: unknown }).role : undefined const invalidRole = Boolean(role) && !['user', 'assistant', 'system'].includes(String(role)) return NextResponse.json( { success: false, error: { message: invalidRole ? 'Message role must be user, assistant, or system' : 'Memory requires messages with role and content', }, }, { status: 400 } ) } } const initialData = Array.isArray(data) ? data : [data] const now = new Date() const id = `mem_${generateId().replace(/-/g, '')}` const { sql } = await import('drizzle-orm') await db .insert(memory) .values({ id, workspaceId, key, data: initialData, createdAt: now, updatedAt: now, }) .onConflictDoUpdate({ target: [memory.workspaceId, memory.key], set: { data: sql`${memory.data} || ${JSON.stringify(initialData)}::jsonb`, updatedAt: now, }, }) logger.info(`[${requestId}] Memory operation successful: ${key} for workspace: ${workspaceId}`) const allMemories = await db .select() .from(memory) .where( and(eq(memory.key, key), eq(memory.workspaceId, workspaceId), isNull(memory.deletedAt)) ) .orderBy(memory.createdAt) if (allMemories.length === 0) { return NextResponse.json( { success: false, error: { message: 'Failed to retrieve memory after creation/update' } }, { status: 500 } ) } const memoryRecord = allMemories[0] return NextResponse.json( { success: true, data: { conversationId: memoryRecord.key, data: memoryRecord.data } }, { status: 200 } ) } catch (error: any) { if (getPostgresErrorCode(error) === '23505') { return NextResponse.json( { success: false, error: { message: 'Memory with this key already exists' } }, { status: 409 } ) } logger.error(`[${requestId}] Error creating memory`, { error }) return NextResponse.json( { success: false, error: { message: 'Failed to create memory' } }, { status: 500 } ) } }) export const DELETE = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const authResult = await checkInternalAuth(request) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized memory deletion attempt`) return NextResponse.json( { success: false, error: { message: authResult.error || 'Authentication required' } }, { status: 401 } ) } const validation = await parseRequest(deleteMemoryByQueryContract, request, {}) if (!validation.success) return validation.response const { workspaceId, conversationId } = validation.data.query if (!workspaceId) { return NextResponse.json( { success: false, error: { message: 'workspaceId parameter is required' } }, { status: 400 } ) } if (!conversationId) { return NextResponse.json( { success: false, error: { message: 'conversationId must be provided' } }, { status: 400 } ) } const access = await checkWorkspaceAccess(workspaceId, authResult.userId) if (!access.exists) { return NextResponse.json( { success: false, error: { message: 'Workspace not found' } }, { status: 404 } ) } if (!access.hasAccess) { return NextResponse.json( { success: false, error: { message: 'Access denied to this workspace' } }, { status: 403 } ) } if (!access.canWrite) { return NextResponse.json( { success: false, error: { message: 'Write access denied to this workspace' } }, { status: 403 } ) } const result = await db .delete(memory) .where( and( eq(memory.key, conversationId), eq(memory.workspaceId, workspaceId), isNull(memory.deletedAt) ) ) .returning({ id: memory.id }) const deletedCount = result.length logger.info(`[${requestId}] Deleted ${deletedCount} memories for workspace: ${workspaceId}`) return NextResponse.json( { success: true, data: { message: deletedCount > 0 ? `Successfully deleted ${deletedCount} memories` : 'No memories found matching the criteria', deletedCount, }, }, { status: 200 } ) } catch (error: any) { logger.error(`[${requestId}] Error deleting memories`, { error }) return NextResponse.json( { success: false, error: { message: error.message || 'Failed to delete memories' } }, { status: 500 } ) } })