Skip to content

Commit ba397f9

Browse files
committed
make memories workspace scoped
1 parent 717b95f commit ba397f9

File tree

12 files changed

+8738
-492
lines changed

12 files changed

+8738
-492
lines changed

apps/sim/app/api/memory/[id]/route.ts

Lines changed: 99 additions & 175 deletions
Large diffs are not rendered by default.

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

Lines changed: 90 additions & 255 deletions
Large diffs are not rendered by default.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,8 +710,8 @@ export class AgentBlockHandler implements BlockHandler {
710710
const conversationMessages = inputMessages.filter((m) => m.role !== 'system')
711711

712712
// 2. Handle native memory: seed on first run, then fetch and append new user input
713-
if (memoryEnabled && ctx.workflowId) {
714-
const hasExisting = await memoryService.hasMemory(ctx.workflowId, inputs.conversationId!)
713+
if (memoryEnabled && ctx.workspaceId) {
714+
const hasExisting = await memoryService.hasMemory(ctx.workspaceId, inputs.conversationId!)
715715

716716
if (!hasExisting && conversationMessages.length > 0) {
717717
await memoryService.seedMemory(ctx, inputs, conversationMessages)

apps/sim/executor/handlers/agent/memory.ts

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import { PROVIDER_DEFINITIONS } from '@/providers/models'
1212
const logger = createLogger('Memory')
1313

1414
export class Memory {
15-
async hasMemory(workflowId: string, conversationId: string): Promise<boolean> {
15+
async hasMemory(workspaceId: string, conversationId: string): Promise<boolean> {
1616
const result = await db
1717
.select({ id: memory.id })
1818
.from(memory)
19-
.where(and(eq(memory.workflowId, workflowId), eq(memory.key, conversationId)))
19+
.where(and(eq(memory.workspaceId, workspaceId), eq(memory.key, conversationId)))
2020
.limit(1)
2121

2222
return result.length > 0
@@ -27,13 +27,10 @@ export class Memory {
2727
return []
2828
}
2929

30-
if (!ctx.workflowId) {
31-
throw new Error('workflowId is required to fetch memory')
32-
}
33-
30+
const workspaceId = this.requireWorkspaceId(ctx)
3431
this.validateConversationId(inputs.conversationId)
3532

36-
const messages = await this.fetchMemory(ctx.workflowId, inputs.conversationId!)
33+
const messages = await this.fetchMemory(workspaceId, inputs.conversationId!)
3734

3835
switch (inputs.memoryType) {
3936
case 'conversation':
@@ -69,10 +66,7 @@ export class Memory {
6966
return
7067
}
7168

72-
if (!ctx.workflowId) {
73-
throw new Error('workflowId is required to append to memory')
74-
}
75-
69+
const workspaceId = this.requireWorkspaceId(ctx)
7670
this.validateConversationId(inputs.conversationId)
7771
this.validateContent(message.content)
7872

@@ -83,23 +77,23 @@ export class Memory {
8377
inputs.slidingWindowSize,
8478
MEMORY.DEFAULT_SLIDING_WINDOW_SIZE
8579
)
86-
const existing = await this.fetchMemory(ctx.workflowId, key)
80+
const existing = await this.fetchMemory(workspaceId, key)
8781
const updated = this.applyWindow([...existing, message], limit)
88-
await this.persistMemory(ctx.workflowId, key, updated)
82+
await this.persistMemory(workspaceId, key, updated)
8983
} else if (inputs.memoryType === 'sliding_window_tokens') {
9084
const maxTokens = this.parsePositiveInt(
9185
inputs.slidingWindowTokens,
9286
MEMORY.DEFAULT_SLIDING_WINDOW_TOKENS
9387
)
94-
const existing = await this.fetchMemory(ctx.workflowId, key)
88+
const existing = await this.fetchMemory(workspaceId, key)
9589
const updated = this.applyTokenWindow([...existing, message], maxTokens, inputs.model)
96-
await this.persistMemory(ctx.workflowId, key, updated)
90+
await this.persistMemory(workspaceId, key, updated)
9791
} else {
98-
await this.appendMessage(ctx.workflowId, key, message)
92+
await this.appendMessage(workspaceId, key, message)
9993
}
10094

10195
logger.debug('Appended message to memory', {
102-
workflowId: ctx.workflowId,
96+
workspaceId,
10397
key,
10498
role: message.role,
10599
})
@@ -110,9 +104,7 @@ export class Memory {
110104
return
111105
}
112106

113-
if (!ctx.workflowId) {
114-
throw new Error('workflowId is required to seed memory')
115-
}
107+
const workspaceId = this.requireWorkspaceId(ctx)
116108

117109
const conversationMessages = messages.filter((m) => m.role !== 'system')
118110
if (conversationMessages.length === 0) {
@@ -138,10 +130,10 @@ export class Memory {
138130
messagesToStore = this.applyTokenWindow(conversationMessages, maxTokens, inputs.model)
139131
}
140132

141-
await this.persistMemory(ctx.workflowId, key, messagesToStore)
133+
await this.persistMemory(workspaceId, key, messagesToStore)
142134

143135
logger.debug('Seeded memory', {
144-
workflowId: ctx.workflowId,
136+
workspaceId,
145137
key,
146138
count: messagesToStore.length,
147139
})
@@ -175,6 +167,13 @@ export class Memory {
175167
return stream.pipeThrough(transformStream)
176168
}
177169

170+
private requireWorkspaceId(ctx: ExecutionContext): string {
171+
if (!ctx.workspaceId) {
172+
throw new Error('workspaceId is required for memory operations')
173+
}
174+
return ctx.workspaceId
175+
}
176+
178177
private applyWindow(messages: Message[], limit: number): Message[] {
179178
return messages.slice(-limit)
180179
}
@@ -222,11 +221,11 @@ export class Memory {
222221
return messages
223222
}
224223

225-
private async fetchMemory(workflowId: string, key: string): Promise<Message[]> {
224+
private async fetchMemory(workspaceId: string, key: string): Promise<Message[]> {
226225
const result = await db
227226
.select({ data: memory.data })
228227
.from(memory)
229-
.where(and(eq(memory.workflowId, workflowId), eq(memory.key, key)))
228+
.where(and(eq(memory.workspaceId, workspaceId), eq(memory.key, key)))
230229
.limit(1)
231230

232231
if (result.length === 0) return []
@@ -239,43 +238,47 @@ export class Memory {
239238
)
240239
}
241240

242-
private async persistMemory(workflowId: string, key: string, messages: Message[]): Promise<void> {
241+
private async persistMemory(
242+
workspaceId: string,
243+
key: string,
244+
messages: Message[]
245+
): Promise<void> {
243246
const now = new Date()
244247

245248
await db
246249
.insert(memory)
247250
.values({
248251
id: randomUUID(),
249-
workflowId,
252+
workspaceId,
250253
key,
251254
data: messages,
252255
createdAt: now,
253256
updatedAt: now,
254257
})
255258
.onConflictDoUpdate({
256-
target: [memory.workflowId, memory.key],
259+
target: [memory.workspaceId, memory.key],
257260
set: {
258261
data: messages,
259262
updatedAt: now,
260263
},
261264
})
262265
}
263266

264-
private async appendMessage(workflowId: string, key: string, message: Message): Promise<void> {
267+
private async appendMessage(workspaceId: string, key: string, message: Message): Promise<void> {
265268
const now = new Date()
266269

267270
await db
268271
.insert(memory)
269272
.values({
270273
id: randomUUID(),
271-
workflowId,
274+
workspaceId,
272275
key,
273276
data: [message],
274277
createdAt: now,
275278
updatedAt: now,
276279
})
277280
.onConflictDoUpdate({
278-
target: [memory.workflowId, memory.key],
281+
target: [memory.workspaceId, memory.key],
279282
set: {
280283
data: sql`${memory.data} || ${JSON.stringify([message])}::jsonb`,
281284
updatedAt: now,

apps/sim/tools/memory/add.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,22 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
4040
'Content-Type': 'application/json',
4141
}),
4242
body: (params) => {
43-
const workflowId = params._context?.workflowId
43+
const workspaceId = params._context?.workspaceId
4444

45-
if (!workflowId) {
45+
if (!workspaceId) {
4646
return {
4747
_errorResponse: {
4848
status: 400,
4949
data: {
5050
success: false,
5151
error: {
52-
message: 'workflowId is required and must be provided in execution context',
52+
message: 'workspaceId is required and must be provided in execution context',
5353
},
5454
},
5555
},
5656
}
5757
}
5858

59-
// Use 'id' as fallback for 'conversationId' for backwards compatibility
6059
const conversationId = params.conversationId || params.id
6160

6261
if (!conversationId || conversationId.trim() === '') {
@@ -91,7 +90,7 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
9190

9291
const body: Record<string, any> = {
9392
key,
94-
workflowId,
93+
workspaceId,
9594
data: {
9695
role: params.role,
9796
content: params.content,

apps/sim/tools/memory/delete.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,22 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
2424

2525
request: {
2626
url: (params): any => {
27-
const workflowId = params._context?.workflowId
27+
const workspaceId = params._context?.workspaceId
2828

29-
if (!workflowId) {
29+
if (!workspaceId) {
3030
return {
3131
_errorResponse: {
3232
status: 400,
3333
data: {
3434
success: false,
3535
error: {
36-
message: 'workflowId is required and must be provided in execution context',
36+
message: 'workspaceId is required and must be provided in execution context',
3737
},
3838
},
3939
},
4040
}
4141
}
4242

43-
// Use 'id' as fallback for 'conversationId' for backwards compatibility
4443
const conversationId = params.conversationId || params.id
4544

4645
if (!conversationId) {
@@ -58,7 +57,7 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
5857
}
5958

6059
const url = new URL('/api/memory', 'http://dummy')
61-
url.searchParams.set('workflowId', workflowId)
60+
url.searchParams.set('workspaceId', workspaceId)
6261
url.searchParams.set('conversationId', conversationId)
6362

6463
return url.pathname + url.search

apps/sim/tools/memory/get.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,22 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
2525

2626
request: {
2727
url: (params): any => {
28-
const workflowId = params._context?.workflowId
28+
const workspaceId = params._context?.workspaceId
2929

30-
if (!workflowId) {
30+
if (!workspaceId) {
3131
return {
3232
_errorResponse: {
3333
status: 400,
3434
data: {
3535
success: false,
3636
error: {
37-
message: 'workflowId is required and must be provided in execution context',
37+
message: 'workspaceId is required and must be provided in execution context',
3838
},
3939
},
4040
},
4141
}
4242
}
4343

44-
// Use 'id' as fallback for 'conversationId' for backwards compatibility
4544
const conversationId = params.conversationId || params.id
4645

4746
if (!conversationId) {
@@ -61,7 +60,7 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
6160
const query = buildMemoryKey(conversationId)
6261

6362
const url = new URL('/api/memory', 'http://dummy')
64-
url.searchParams.set('workflowId', workflowId)
63+
url.searchParams.set('workspaceId', workspaceId)
6564
url.searchParams.set('query', query)
6665
url.searchParams.set('limit', '1000')
6766

apps/sim/tools/memory/get_all.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,23 @@ export const memoryGetAllTool: ToolConfig<any, MemoryResponse> = {
1111

1212
request: {
1313
url: (params): any => {
14-
const workflowId = params._context?.workflowId
14+
const workspaceId = params._context?.workspaceId
1515

16-
if (!workflowId) {
16+
if (!workspaceId) {
1717
return {
1818
_errorResponse: {
1919
status: 400,
2020
data: {
2121
success: false,
2222
error: {
23-
message: 'workflowId is required and must be provided in execution context',
23+
message: 'workspaceId is required and must be provided in execution context',
2424
},
2525
},
2626
},
2727
}
2828
}
2929

30-
return `/api/memory?workflowId=${encodeURIComponent(workflowId)}`
30+
return `/api/memory?workspaceId=${encodeURIComponent(workspaceId)}`
3131
},
3232
method: 'GET',
3333
headers: () => ({
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
-- Step 1: Add workspace_id as nullable first
2+
ALTER TABLE "memory" ADD COLUMN "workspace_id" text;
3+
4+
-- Step 2: Backfill workspace_id from workflow's workspace_id
5+
UPDATE memory m
6+
SET workspace_id = w.workspace_id
7+
FROM workflow w
8+
WHERE m.workflow_id = w.id
9+
AND w.workspace_id IS NOT NULL;
10+
11+
-- Step 3: Delete rows where workspace_id couldn't be resolved
12+
DELETE FROM memory WHERE workspace_id IS NULL;
13+
14+
-- Step 4: Now make workspace_id NOT NULL
15+
ALTER TABLE "memory" ALTER COLUMN "workspace_id" SET NOT NULL;
16+
17+
-- Step 5: Drop old constraint and indexes
18+
ALTER TABLE "memory" DROP CONSTRAINT IF EXISTS "memory_workflow_id_workflow_id_fk";
19+
--> statement-breakpoint
20+
DROP INDEX IF EXISTS "memory_workflow_idx";
21+
--> statement-breakpoint
22+
DROP INDEX IF EXISTS "memory_workflow_key_idx";
23+
24+
-- Step 6: Add new foreign key and indexes
25+
ALTER TABLE "memory" ADD CONSTRAINT "memory_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;
26+
--> statement-breakpoint
27+
CREATE INDEX "memory_workspace_idx" ON "memory" USING btree ("workspace_id");
28+
--> statement-breakpoint
29+
CREATE UNIQUE INDEX "memory_workspace_key_idx" ON "memory" USING btree ("workspace_id","key");
30+
31+
-- Step 7: Drop old column
32+
ALTER TABLE "memory" DROP COLUMN IF EXISTS "workflow_id";

0 commit comments

Comments
 (0)