Skip to content

Commit a37efa7

Browse files
committed
fix(chat): give a personal organization's chats their member's personal memory graph
A workspace chat in a personal organization already names no organization in its memory scope (the payer rule), so the worker keeps it on the personal graph. An organization chat in the same personal organization still named the organization, so it read and wrote a second graph its workspace chats never saw. The scope now applies the payer rule to the chat's organization too: a personal organization's chats share their one member's personal graph, and a team's chats keep naming the team. Needs mothership fix/personal-org-chat-memory (D115) deployed first: until then the worker refuses an organization chat whose scope names no organization, and memory for those chats is denied.
1 parent ffaf52c commit a37efa7

3 files changed

Lines changed: 35 additions & 7 deletions

File tree

‎apps/sim/lib/mothership/memory/application/read-scope.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function queueChat(mode = 'plan', membership = true) {
3636
{ userId: 'actor', organizationId: 'org-1', workspaceId: null, type: 'mothership', mode },
3737
])
3838
queueTableRows(member, membership ? [{ role: 'member' }] : [])
39+
queueTableRows(organization, [{ id: 'org-1' }])
3940
}
4041
describe('private memory scope', () => {
4142
beforeEach(() => {
@@ -72,6 +73,7 @@ describe('private memory scope', () => {
7273
},
7374
])
7475
queueTableRows(member, [{ role: 'member' }])
76+
queueTableRows(organization, [{ id: 'org-1' }])
7577
expect(await readMemoryScope.execute({ principal: principal(), input })).toEqual({
7678
enabled: false,
7779
userId: 'actor',

‎apps/sim/lib/mothership/memory/application/read-scope.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,9 @@ export const readMemoryScope = defineAuthorizedChatUseCase({
6565
)
6666
}
6767
// The worker keys a graph by this id (`["organization", id]`, else `["personal"]`). A personal
68-
// workspace moved into its owner's personal organization keeps the personal graph it had, so a
69-
// workspace chat names only a team (the payer rule); an organization chat names its own.
70-
const graphOrganizationId =
71-
context.organizationId ??
72-
(await resolvePayerOrganizationId(db, context.workspaceOrganizationId ?? null))
68+
// organization has one member, so its workspace and organization chats share that member's
69+
// personal graph: the scope names only a team (the payer rule).
70+
const graphOrganizationId = await resolvePayerOrganizationId(db, spaceOrganizationId)
7371
return {
7472
enabled,
7573
...(spaceId ? { spaceId } : {}),

‎apps/sim/lib/mothership/memory/application/spaces.integration.ts‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,20 @@ const create = (name: string) =>
8080
const select = (spaceId: string | null) =>
8181
selectMemorySpace.execute({ principal: principal(), input: { ...input, spaceId } })
8282
const list = () => listMemorySpaces.execute({ principal: principal(), input })
83-
async function scope(chatId: string, workspaceId?: string, userId = ids.owner) {
83+
async function scope(
84+
chatId: string,
85+
workspaceId?: string,
86+
userId = ids.owner,
87+
organizationId = ids.org
88+
) {
8489
const options = { audience: MEMORY_SCOPE_AUDIENCE, ttlMs: 60_000 }
8590
const caller = workspaceId
8691
? createTrustedCopilotPrincipal(
8792
{ userId, workspaceId, chatId, delegationId: generateId() },
8893
options
8994
)
9095
: createTrustedOrganizationCopilotPrincipal(
91-
{ userId, organizationId: ids.org, chatId, delegationId: generateId() },
96+
{ userId, organizationId, chatId, delegationId: generateId() },
9297
options
9398
)
9499
return readMemoryScope.execute({
@@ -637,6 +642,29 @@ describe('a workspace chat moved into its owner’s personal organization', () =
637642
})
638643
})
639644

645+
it('shares that personal graph with an organization chat in the same personal organization', async () => {
646+
const workspaceChatId = await workspaceChat(ids.personalWorkspace)
647+
const organizationChat = await createOrganizationChatRecord(
648+
{ userId: ids.personalOwner, organizationId: personalOrganizationId },
649+
'agent'
650+
)
651+
652+
const workspaceGraph = await scope(workspaceChatId, ids.personalWorkspace, ids.personalOwner)
653+
expect(
654+
await scope(organizationChat.id, undefined, ids.personalOwner, personalOrganizationId)
655+
).toEqual({
656+
enabled: true,
657+
userId: ids.personalOwner,
658+
organizationId: workspaceGraph.organizationId,
659+
workspaceId: null,
660+
})
661+
const teamChat = await createOrganizationChatRecord(
662+
{ userId: ids.other, organizationId: ids.org },
663+
'agent'
664+
)
665+
expect((await scope(teamChat.id, undefined, ids.other)).organizationId).toBe(ids.org)
666+
})
667+
640668
it('keeps the default graph when a chosen space has no organization to own it', async () => {
641669
const chatId = await workspaceChat(ids.unownedWorkspace, generateId())
642670

0 commit comments

Comments
 (0)