Skip to content

Commit 3e83243

Browse files
committed
backend for new memory setup, backwards compatibility in loadWorkflowsFromNormalizedTable from old agent block to new format
1 parent c89d444 commit 3e83243

File tree

10 files changed

+1217
-141
lines changed

10 files changed

+1217
-141
lines changed

apps/sim/app/api/memory/route.ts

Lines changed: 31 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -193,75 +193,40 @@ export async function POST(request: NextRequest) {
193193
}
194194
}
195195

196-
// Check if memory with the same key already exists for this workflow
197-
const existingMemory = await db
198-
.select()
199-
.from(memory)
200-
.where(and(eq(memory.key, key), eq(memory.workflowId, workflowId), isNull(memory.deletedAt)))
201-
.limit(1)
202-
203-
let statusCode = 201 // Default status code for new memory
204-
205-
if (existingMemory.length > 0) {
206-
logger.info(`[${requestId}] Memory with key ${key} exists, checking if we can append`)
207-
208-
// Check if types match
209-
if (existingMemory[0].type !== type) {
210-
logger.warn(
211-
`[${requestId}] Memory type mismatch: existing=${existingMemory[0].type}, new=${type}`
212-
)
213-
return NextResponse.json(
214-
{
215-
success: false,
216-
error: {
217-
message: `Cannot append memory of type '${type}' to existing memory of type '${existingMemory[0].type}'`,
218-
},
219-
},
220-
{ status: 400 }
221-
)
222-
}
223-
224-
// Handle appending for agent type
225-
let updatedData
226-
227-
// For agent type
228-
const newMessage = data
229-
const existingData = existingMemory[0].data
230-
231-
// If existing data is an array, append to it
232-
if (Array.isArray(existingData)) {
233-
updatedData = [...existingData, newMessage]
234-
}
235-
// If existing data is a single message object, convert to array
236-
else {
237-
updatedData = [existingData, newMessage]
238-
}
239-
240-
// Update the existing memory with appended data
241-
await db
242-
.update(memory)
243-
.set({
244-
data: updatedData,
245-
updatedAt: new Date(),
246-
})
247-
.where(and(eq(memory.key, key), eq(memory.workflowId, workflowId)))
248-
249-
statusCode = 200 // Status code for updated memory
250-
} else {
251-
// Insert the new memory
252-
const newMemory = {
253-
id: `mem_${crypto.randomUUID().replace(/-/g, '')}`,
196+
// Use atomic UPSERT with JSONB append to prevent race conditions
197+
// If data is an array, insert it directly; if single message, wrap in array
198+
const initialData = Array.isArray(data) ? data : [data]
199+
const now = new Date()
200+
const id = `mem_${crypto.randomUUID().replace(/-/g, '')}`
201+
202+
// Import sql helper for raw SQL in Drizzle
203+
const { sql } = await import('drizzle-orm')
204+
205+
// Atomically insert or append using PostgreSQL JSONB concatenation
206+
await db
207+
.insert(memory)
208+
.values({
209+
id,
254210
workflowId,
255211
key,
256212
type,
257-
data: Array.isArray(data) ? data : [data],
258-
createdAt: new Date(),
259-
updatedAt: new Date(),
260-
}
213+
data: initialData,
214+
createdAt: now,
215+
updatedAt: now,
216+
})
217+
.onConflictDoUpdate({
218+
target: [memory.workflowId, memory.key],
219+
set: {
220+
// Atomically append: data = data || '[new_message]'::jsonb
221+
// This prevents lost updates in concurrent scenarios
222+
data: sql`${memory.data} || ${JSON.stringify(initialData)}::jsonb`,
223+
updatedAt: now,
224+
},
225+
})
261226

262-
await db.insert(memory).values(newMemory)
263-
logger.info(`[${requestId}] Memory created successfully: ${key} for workflow: ${workflowId}`)
264-
}
227+
logger.info(
228+
`[${requestId}] Memory operation successful (atomic): ${key} for workflow: ${workflowId}`
229+
)
265230

266231
// Fetch all memories with the same key for this workflow to return the complete list
267232
const allMemories = await db
@@ -287,13 +252,12 @@ export async function POST(request: NextRequest) {
287252
// Get the memory object to return
288253
const memoryRecord = allMemories[0]
289254

290-
logger.info(`[${requestId}] Memory operation successful: ${key} for workflow: ${workflowId}`)
291255
return NextResponse.json(
292256
{
293257
success: true,
294258
data: memoryRecord,
295259
},
296-
{ status: statusCode }
260+
{ status: 200 }
297261
)
298262
} catch (error: any) {
299263
// Handle unique constraint violation

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,42 +36,44 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
3636
remarkPlugins={[remarkGfm]}
3737
components={{
3838
p: ({ children }) => (
39-
<p className='!mt-0 !-mb-4 text-[#E5E5E5] text-sm leading-tight'>{children}</p>
39+
<p className='!mt-0 !-mb-1 break-words text-[#E5E5E5] text-sm leading-tight'>
40+
{children}
41+
</p>
4042
),
4143
h1: ({ children }) => (
42-
<h1 className='!mt-0 !-mb-4 font-semibold text-[#E5E5E5] text-lg leading-tight'>
44+
<h1 className='!mt-0 !-mb-4 break-words font-semibold text-[#E5E5E5] text-lg leading-tight'>
4345
{children}
4446
</h1>
4547
),
4648
h2: ({ children }) => (
47-
<h2 className='!mt-0 !-mb-4 font-semibold text-[#E5E5E5] text-base leading-tight'>
49+
<h2 className='!mt-0 !-mb-4 break-words font-semibold text-[#E5E5E5] text-base leading-tight'>
4850
{children}
4951
</h2>
5052
),
5153
h3: ({ children }) => (
52-
<h3 className='!mt-0 !-mb-4 font-semibold text-[#E5E5E5] text-sm leading-tight'>
54+
<h3 className='!mt-0 !-mb-4 break-words font-semibold text-[#E5E5E5] text-sm leading-tight'>
5355
{children}
5456
</h3>
5557
),
5658
h4: ({ children }) => (
57-
<h4 className='!mt-0 !-mb-4 font-semibold text-[#E5E5E5] text-xs leading-tight'>
59+
<h4 className='!mt-0 !-mb-4 break-words font-semibold text-[#E5E5E5] text-xs leading-tight'>
5860
{children}
5961
</h4>
6062
),
6163
ul: ({ children }) => (
62-
<ul className='!-mt-4 !-mb-4 [&_li>ul]:!mt-0 [&_li>ul]:!mb-0 [&_li>ol]:!mt-0 [&_li>ol]:!mb-0 list-disc pl-4 text-[#E5E5E5] text-sm leading-tight'>
64+
<ul className='!-mt-4 !-mb-4 [&_li>ul]:!mt-0 [&_li>ul]:!mb-0 [&_li>ol]:!mt-0 [&_li>ol]:!mb-0 list-disc break-words pl-4 text-[#E5E5E5] text-sm leading-tight'>
6365
{children}
6466
</ul>
6567
),
6668
ol: ({ children }) => (
67-
<ol className='!-mt-4 !-mb-4 [&_li>ul]:!mt-0 [&_li>ul]:!mb-0 [&_li>ol]:!mt-0 [&_li>ol]:!mb-0 list-decimal pl-4 text-[#E5E5E5] text-sm leading-tight'>
69+
<ol className='!-mt-4 !-mb-4 [&_li>ul]:!mt-0 [&_li>ul]:!mb-0 [&_li>ol]:!mt-0 [&_li>ol]:!mb-0 list-decimal break-words pl-4 text-[#E5E5E5] text-sm leading-tight'>
6870
{children}
6971
</ol>
7072
),
71-
li: ({ children }) => <li className='!mb-0 leading-tight'>{children}</li>,
73+
li: ({ children }) => <li className='!mb-0 break-words leading-tight'>{children}</li>,
7274
code: ({ inline, children }: any) =>
7375
inline ? (
74-
<code className='break-words rounded bg-[var(--divider)] px-1 py-0.5 text-[#F59E0B] text-xs'>
76+
<code className='inline whitespace-nowrap rounded bg-[var(--divider)] px-1 py-0.5 text-[#F59E0B] text-xs'>
7577
{children}
7678
</code>
7779
) : (
@@ -89,10 +91,12 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
8991
{children}
9092
</a>
9193
),
92-
strong: ({ children }) => <strong className='font-semibold text-white'>{children}</strong>,
93-
em: ({ children }) => <em className='text-[#B8B8B8]'>{children}</em>,
94+
strong: ({ children }) => (
95+
<strong className='break-words font-semibold text-white'>{children}</strong>
96+
),
97+
em: ({ children }) => <em className='break-words text-[#B8B8B8]'>{children}</em>,
9498
blockquote: ({ children }) => (
95-
<blockquote className='!mt-0 !-mb-4 border-[#F59E0B] border-l-2 pl-3 text-[#B8B8B8] italic'>
99+
<blockquote className='!-mt-1 !-mb-4 break-words border-[#F59E0B] border-l-2 pl-3 text-[#B8B8B8] italic'>
96100
{children}
97101
</blockquote>
98102
),

apps/sim/blocks/blocks/agent.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
186186
}),
187187
},
188188
{
189-
id: 'memory',
189+
id: 'memoryType',
190190
title: 'Memory',
191191
type: 'dropdown',
192192
placeholder: 'Select memory...',
@@ -204,7 +204,7 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
204204
type: 'short-input',
205205
placeholder: 'user-1234',
206206
condition: {
207-
field: 'memory',
207+
field: 'memoryType',
208208
value: ['conversation_id'],
209209
},
210210
},
@@ -214,7 +214,7 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
214214
type: 'short-input',
215215
placeholder: 'Enter number of messages (e.g., 10)...',
216216
condition: {
217-
field: 'memory',
217+
field: 'memoryType',
218218
value: ['sliding_window'],
219219
},
220220
},

apps/sim/executor/handlers/agent/agent-handler.test.ts

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,11 +1145,13 @@ describe('AgentBlockHandler', () => {
11451145
expect(systemMessages[0].content).toBe('You are a helpful assistant.')
11461146
})
11471147

1148-
it('should prioritize explicit systemPrompt over system messages in memories', async () => {
1148+
it('should prioritize messages array system message over system messages in memories', async () => {
11491149
const inputs = {
11501150
model: 'gpt-4o',
1151-
systemPrompt: 'You are a helpful assistant.',
1152-
userPrompt: 'What should I do?',
1151+
messages: [
1152+
{ role: 'system', content: 'You are a helpful assistant.' },
1153+
{ role: 'user', content: 'What should I do?' },
1154+
],
11531155
memories: [
11541156
{ role: 'system', content: 'Old system message from memories.' },
11551157
{ role: 'user', content: 'Hello!' },
@@ -1167,31 +1169,30 @@ describe('AgentBlockHandler', () => {
11671169

11681170
// Verify messages were built correctly
11691171
expect(requestBody.messages).toBeDefined()
1170-
expect(requestBody.messages.length).toBe(4) // explicit system + 2 non-system memories + user prompt
1171-
1172-
// Check only one system message exists and it's the explicit one
1173-
const systemMessages = requestBody.messages.filter((msg: any) => msg.role === 'system')
1174-
expect(systemMessages.length).toBe(1)
1175-
expect(systemMessages[0].content).toBe('You are a helpful assistant.')
1172+
expect(requestBody.messages.length).toBe(5) // memory system + 2 non-system memories + 2 from messages array
11761173

1177-
// Verify the explicit system prompt is first
1174+
// All messages should be present (memories first, then messages array)
1175+
// Memory messages come first
11781176
expect(requestBody.messages[0].role).toBe('system')
1179-
expect(requestBody.messages[0].content).toBe('You are a helpful assistant.')
1180-
1181-
// Verify conversation order is preserved
1177+
expect(requestBody.messages[0].content).toBe('Old system message from memories.')
11821178
expect(requestBody.messages[1].role).toBe('user')
11831179
expect(requestBody.messages[1].content).toBe('Hello!')
11841180
expect(requestBody.messages[2].role).toBe('assistant')
11851181
expect(requestBody.messages[2].content).toBe('Hi there!')
1186-
expect(requestBody.messages[3].role).toBe('user')
1187-
expect(requestBody.messages[3].content).toBe('What should I do?')
1182+
// Then messages array
1183+
expect(requestBody.messages[3].role).toBe('system')
1184+
expect(requestBody.messages[3].content).toBe('You are a helpful assistant.')
1185+
expect(requestBody.messages[4].role).toBe('user')
1186+
expect(requestBody.messages[4].content).toBe('What should I do?')
11881187
})
11891188

1190-
it('should handle multiple system messages in memories with explicit systemPrompt', async () => {
1189+
it('should handle multiple system messages in memories with messages array', async () => {
11911190
const inputs = {
11921191
model: 'gpt-4o',
1193-
systemPrompt: 'You are a helpful assistant.',
1194-
userPrompt: 'Continue our conversation.',
1192+
messages: [
1193+
{ role: 'system', content: 'You are a helpful assistant.' },
1194+
{ role: 'user', content: 'Continue our conversation.' },
1195+
],
11951196
memories: [
11961197
{ role: 'system', content: 'First system message.' },
11971198
{ role: 'user', content: 'Hello!' },
@@ -1211,22 +1212,23 @@ describe('AgentBlockHandler', () => {
12111212

12121213
// Verify messages were built correctly
12131214
expect(requestBody.messages).toBeDefined()
1214-
expect(requestBody.messages.length).toBe(4) // explicit system + 2 non-system memories + user prompt
1215-
1216-
// Check only one system message exists and message order is preserved
1217-
const systemMessages = requestBody.messages.filter((msg: any) => msg.role === 'system')
1218-
expect(systemMessages.length).toBe(1)
1219-
expect(systemMessages[0].content).toBe('You are a helpful assistant.')
1215+
expect(requestBody.messages.length).toBe(7) // 5 memory messages (3 system + 2 conversation) + 2 from messages array
12201216

1221-
// Verify conversation flow is preserved
1217+
// All messages should be present in order
12221218
expect(requestBody.messages[0].role).toBe('system')
1223-
expect(requestBody.messages[0].content).toBe('You are a helpful assistant.')
1219+
expect(requestBody.messages[0].content).toBe('First system message.')
12241220
expect(requestBody.messages[1].role).toBe('user')
12251221
expect(requestBody.messages[1].content).toBe('Hello!')
1226-
expect(requestBody.messages[2].role).toBe('assistant')
1227-
expect(requestBody.messages[2].content).toBe('Hi there!')
1228-
expect(requestBody.messages[3].role).toBe('user')
1229-
expect(requestBody.messages[3].content).toBe('Continue our conversation.')
1222+
expect(requestBody.messages[2].role).toBe('system')
1223+
expect(requestBody.messages[2].content).toBe('Second system message.')
1224+
expect(requestBody.messages[3].role).toBe('assistant')
1225+
expect(requestBody.messages[3].content).toBe('Hi there!')
1226+
expect(requestBody.messages[4].role).toBe('system')
1227+
expect(requestBody.messages[4].content).toBe('Third system message.')
1228+
expect(requestBody.messages[5].role).toBe('system')
1229+
expect(requestBody.messages[5].content).toBe('You are a helpful assistant.')
1230+
expect(requestBody.messages[6].role).toBe('user')
1231+
expect(requestBody.messages[6].content).toBe('Continue our conversation.')
12301232
})
12311233

12321234
it('should preserve multiple system messages when no explicit systemPrompt is provided', async () => {

0 commit comments

Comments
 (0)