Skip to content

Commit 405add8

Browse files
committed
Expose the Graphiti rollout gate through authorized memory scope
1 parent cdf89ad commit 405add8

6 files changed

Lines changed: 43 additions & 5 deletions

File tree

‎apps/sim/lib/core/config/feature-flags.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const FEATURE_FLAGS = {
6969
fallback: 'MSHIP_MODEL_SELECTOR',
7070
},
7171
'mothership-memory-spaces': {
72-
description: 'Create and select private knowledge graphs for new Mothership chats.',
72+
description: 'Enable private Graphiti memory and its graph-management settings in Mothership.',
7373
fallback: null,
7474
},
7575
'mothership-plan-mode': {

‎apps/sim/lib/mothership/feature-flags.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function isSearchIntegrationToolsEnabled(): Promise<boolean> {
1515
return isFeatureEnabled('mothership-search-integration-tools')
1616
}
1717

18-
/** Graph management is enabled only through AppConfig. */
18+
/** One AppConfig gate controls private Graphiti memory and graph management. */
1919
export function isMemorySpacesEnabled(): Promise<boolean> {
2020
return isFeatureEnabled('mothership-memory-spaces')
2121
}

‎apps/sim/lib/mothership/generated/memory-scope.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { z } from "zod";
66
export const MemoryScopeRequest = z.object({ chatId: z.uuid() });
77
export type MemoryScopeRequest = z.infer<typeof MemoryScopeRequest>;
88
export const MemoryScopeResponse = z.object({
9+
enabled: z.boolean().default(false),
910
userId: z.string().min(1),
1011
organizationId: z.string().min(1).nullable(),
1112
workspaceId: z.uuid().nullable(),

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { copilotChats, member, workspace } from '@sim/db/schema'
33
import { queueTableRows, resetDbChainMock } from '@sim/testing'
44
import { authBanMock, authBanMockFns } from '@sim/testing/mocks/auth-ban.mock'
5+
import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock'
56
import {
67
permissionGroupsResolveMock,
78
permissionGroupsResolveMockFns,
@@ -21,6 +22,7 @@ vi.mock('@/lib/permission-groups/capability-assertions', async (importOriginal)
2122
...(await importOriginal<typeof import('@/lib/permission-groups/capability-assertions')>()),
2223
assertWorkspaceCapability: mocks.capability,
2324
}))
25+
vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock)
2426
const input = { chatId: 'chat-1' }
2527
function principal() {
2628
return createTrustedOrganizationCopilotPrincipal(
@@ -37,6 +39,7 @@ function queueChat(mode = 'plan', membership = true) {
3739
describe('private memory scope', () => {
3840
beforeEach(() => {
3941
resetDbChainMock()
42+
featureFlagsMockFns.mockIsFeatureEnabled.mockResolvedValue(true)
4043
authBanMockFns.mockGetActivelyBannedUserIds.mockResolvedValue([])
4144
permissionGroupsResolveMockFns.mockGetUserPermissionConfigForOrganization.mockResolvedValue(
4245
null
@@ -47,11 +50,33 @@ describe('private memory scope', () => {
4750
it.each(['plan', 'agent'])('resolves the current %s chat owner', async (mode) => {
4851
queueChat(mode)
4952
expect(await readMemoryScope.execute({ principal: principal(), input })).toEqual({
53+
enabled: true,
5054
userId: 'actor',
5155
organizationId: 'org-1',
5256
workspaceId: null,
5357
})
5458
})
59+
it('reports Graphiti disabled independently of the chat mode and preserves its graph binding', async () => {
60+
featureFlagsMockFns.mockIsFeatureEnabled.mockResolvedValue(false)
61+
queueTableRows(copilotChats, [
62+
{
63+
userId: 'actor',
64+
organizationId: 'org-1',
65+
workspaceId: null,
66+
type: 'mothership',
67+
mode: 'plan',
68+
memorySpaceId: 'saved-graph',
69+
},
70+
])
71+
queueTableRows(member, [{ role: 'member' }])
72+
expect(await readMemoryScope.execute({ principal: principal(), input })).toEqual({
73+
enabled: false,
74+
userId: 'actor',
75+
organizationId: 'org-1',
76+
workspaceId: null,
77+
spaceId: 'saved-graph',
78+
})
79+
})
5580
it.each(['user', 'organization', 'chat', 'audience', 'expired'] as const)(
5681
'rejects forged %s',
5782
async (field) => {
@@ -105,6 +130,7 @@ describe('private memory scope', () => {
105130
)
106131
queueWorkspace()
107132
expect(await readMemoryScope.execute({ principal: caller, input })).toEqual({
133+
enabled: true,
108134
userId: 'actor',
109135
organizationId: 'canonical-org',
110136
workspaceId: 'workspace',

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { defineOrganizationOperation } from '@/lib/core/application/organization
44
import { OrchestrationError } from '@/lib/core/orchestration/types'
55
import { defineAuthorizedChatUseCase } from '@/lib/mothership/chat/application/authorized-chat-use-case'
66
import { resolveOwnedChatContext } from '@/lib/mothership/chat/application/context'
7+
import { isMemorySpacesEnabled } from '@/lib/mothership/feature-flags'
78
import type { MemoryScopeRequest } from '@/lib/mothership/generated/memory-scope'
89
import { requireOwnedMemorySpace } from '@/lib/mothership/memory/spaces'
910

@@ -48,15 +49,17 @@ export const readMemoryScope = defineAuthorizedChatUseCase({
4849
if (context.mode !== 'agent' && context.mode !== 'plan') {
4950
throw new OrchestrationError('forbidden', 'Private memory requires an interactive agent chat')
5051
}
52+
const enabled = await isMemorySpacesEnabled()
5153
const organizationId = context.organizationId ?? context.workspaceOrganizationId ?? null
52-
if (context.memorySpaceId) {
54+
if (enabled && context.memorySpaceId) {
5355
if (!organizationId) throw new OrchestrationError('not_found', 'Knowledge graph not found')
5456
await requireOwnedMemorySpace(
5557
{ userId: context.userId, organizationId },
5658
context.memorySpaceId
5759
)
5860
}
5961
return {
62+
enabled,
6063
...(context.memorySpaceId ? { spaceId: context.memorySpaceId } : {}),
6164
userId: context.userId,
6265
organizationId: context.organizationId ?? context.workspaceOrganizationId ?? null,

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ describe('private KG selection through authorized application boundaries', () =>
229229
expect(after.spaces.some((space) => space.id === row.activeSpaceId)).toBe(true)
230230
})
231231

232-
it('disabling management preserves bound graphs and makes new chats use Default', async () => {
232+
it('disabling Graphiti preserves bound graphs for reactivation and makes new chats use Default', async () => {
233233
const selected = await create('Flag lifecycle')
234234
const chat = await createOrganizationChat.execute({
235235
principal: principal(),
@@ -240,7 +240,10 @@ describe('private KG selection through authorized application boundaries', () =>
240240
await expect(list()).rejects.toMatchObject({ code: 'not_found' })
241241
await expect(create('Hidden')).rejects.toMatchObject({ code: 'not_found' })
242242
await expect(select(null)).rejects.toMatchObject({ code: 'not_found' })
243-
expect((await scope(chat.id)).spaceId).toBe(selected.activeSpaceId)
243+
expect(await scope(chat.id)).toMatchObject({
244+
enabled: false,
245+
spaceId: selected.activeSpaceId,
246+
})
244247
const next = await createOrganizationChat.execute({
245248
principal: principal(),
246249
input: { ...input, mode: 'plan' },
@@ -249,6 +252,11 @@ describe('private KG selection through authorized application boundaries', () =>
249252
} finally {
250253
gate.enabled = true
251254
}
255+
expect(await scope(chat.id)).toMatchObject({ enabled: true, spaceId: selected.activeSpaceId })
256+
expect((await list()).spaces).toContainEqual({
257+
id: selected.activeSpaceId,
258+
name: 'Flag lifecycle',
259+
})
252260
})
253261

254262
it('rejects a foreign binding and rechecks membership before reading memory', async () => {

0 commit comments

Comments
 (0)