Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d3fbcf9
move offline agents filtering to query root
nazabucciarelli Mar 10, 2026
04dc75a
fix user presence update from test users
nazabucciarelli Mar 11, 2026
81cf18e
fix linter error
nazabucciarelli Mar 11, 2026
391cce0
Merge branch 'develop' into fix/livechat-offline-agent-assignment
nazabucciarelli Mar 11, 2026
cae2152
rollback TEST_MODE conditional
nazabucciarelli Mar 11, 2026
0942d4a
add ws user helpers to fix connectionStatus on tests
nazabucciarelli Mar 12, 2026
9102e16
test on CI without closing ws
nazabucciarelli Mar 12, 2026
395c57b
add use of setUserAwayWS to routing test
nazabucciarelli Mar 12, 2026
6ebec42
enhance ws connection, add ws closures
nazabucciarelli Mar 13, 2026
8b41cd9
change ws port to 3000
nazabucciarelli Mar 13, 2026
6f3c33f
update idle status tests from routing
nazabucciarelli Mar 13, 2026
79d8eb9
fix playwright tests
nazabucciarelli Mar 16, 2026
46d52d0
add ws closure to playwright tests
nazabucciarelli Mar 16, 2026
6c3b010
improve ws cleanup on rooms test, make ws port depend on CI env variable
nazabucciarelli Mar 16, 2026
2780e2c
pass the acceptChatsWithNoAgents as flag to agent routing methods
nazabucciarelli Mar 16, 2026
8137749
fix conflict with develop
nazabucciarelli Mar 17, 2026
7c2f150
remove unintended test lines
nazabucciarelli Mar 17, 2026
fcd37ad
Merge branch 'develop' into fix/livechat-offline-agent-assignment
nazabucciarelli Mar 17, 2026
02239ef
add changeset
nazabucciarelli Mar 17, 2026
ba26d7f
remove unneeded ws closures
nazabucciarelli Mar 17, 2026
e287b5b
handle errors on websockets result
nazabucciarelli Mar 17, 2026
cf2ee47
add auto selection routing test
nazabucciarelli Mar 18, 2026
1ae6230
remove accidental timeout
nazabucciarelli Mar 18, 2026
3f9abbc
bring related branch changes
nazabucciarelli Mar 18, 2026
81fe8ad
fix auto selection tests
nazabucciarelli Mar 18, 2026
8883278
add load rotation test
nazabucciarelli Mar 18, 2026
dbeef1b
add load balancing test
nazabucciarelli Mar 18, 2026
6e47218
refactor tests
nazabucciarelli Mar 18, 2026
6134a5c
add DDP_LOGIN_PORT env variable for ws connection
nazabucciarelli Mar 19, 2026
f6e4a54
Merge branch 'develop' into fix/livechat-offline-agent-assignment
nazabucciarelli Mar 19, 2026
fca1d9a
Correct phrasing in changeset description
nazabucciarelli Mar 19, 2026
0b3395c
add forgotten CI new env variable
nazabucciarelli Mar 19, 2026
e74d5af
Merge branch 'fix/livechat-offline-agent-assignment' of github.com:Ro…
nazabucciarelli Mar 19, 2026
6d760f1
safe ws closure
nazabucciarelli Mar 19, 2026
7199cc4
enhance overall WS connection logic
nazabucciarelli Mar 20, 2026
d808a27
add onError handler for ws
nazabucciarelli Mar 20, 2026
7fc1f0f
Merge branch 'fix/livechat-offline-agent-assignment' of github.com:Ro…
nazabucciarelli Mar 20, 2026
1bebc26
extract setting value in a const and add missing value in error message
nazabucciarelli Mar 20, 2026
027d8ed
wip
nazabucciarelli Mar 20, 2026
2bdff92
fix remaining routing tests
nazabucciarelli Mar 20, 2026
753a028
add setting in missing places
nazabucciarelli Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/purple-boxes-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/models': patch
'@rocket.chat/meteor': patch
---

Fixes an issue where offline livechat agents were being assigned to visitors.
1 change: 1 addition & 0 deletions .github/workflows/ci-test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ env:
TOOL_NODE_FLAGS: ${{ vars.TOOL_NODE_FLAGS }}
LOWERCASE_REPOSITORY: ${{ inputs.lowercase-repo }}
DOCKER_TAG: ${{ inputs.gh-docker-tag }}-amd64
DDP_LOGIN_PORT: 3000

jobs:
test:
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/app/apps/server/bridges/livechat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export class AppLivechatBridge extends LivechatBridge {

const livechatVisitor = await registerGuest(registerData, {
shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle'),
shouldConsiderOfflineAgent: settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
});

if (!livechatVisitor) {
Expand All @@ -239,6 +240,7 @@ export class AppLivechatBridge extends LivechatBridge {

const livechatVisitor = await registerGuest(registerData, {
shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle'),
shouldConsiderOfflineAgent: settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
});

return this.orch.getConverters()?.get('visitors').convertVisitor(livechatVisitor);
Expand Down
3 changes: 2 additions & 1 deletion apps/meteor/app/livechat/imports/server/rest/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ const defineVisitor = async (smsNumber: string, targetDepartment?: string) => {
data.department = targetDepartment;
}

const livechatVisitor = await registerGuest(data, { shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle') });
const livechatVisitor = await registerGuest(data, { shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle'),
shouldConsiderOfflineAgent: settings.get<boolean>('Livechat_accept_chats_with_no_agents')});

if (!livechatVisitor) {
throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor');
Expand Down
5 changes: 4 additions & 1 deletion apps/meteor/app/livechat/server/api/v1/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,10 @@ API.v1.addRoute(
guest.connectionData = normalizeHttpHeaderData(this.request.headers);
}

visitor = await registerGuest(guest, { shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle') });
visitor = await registerGuest(guest, {
shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle'),
shouldConsiderOfflineAgent: settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
});
if (!visitor) {
throw new Error('error-livechat-visitor-registration');
}
Expand Down
5 changes: 4 additions & 1 deletion apps/meteor/app/livechat/server/api/v1/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ API.v1.addRoute(
connectionData: normalizeHttpHeaderData(this.request.headers),
};

const visitor = await registerGuest(guest, { shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle') });
const visitor = await registerGuest(guest, {
shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle'),
shouldConsiderOfflineAgent: settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
});
if (!visitor) {
throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', {
method: 'livechat/visitor',
Expand Down
16 changes: 13 additions & 3 deletions apps/meteor/app/livechat/server/lib/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,12 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T
if (!agentId) {
throw new Error('error-invalid-agent');
}
const user = await Users.findOneOnlineAgentById(agentId, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
const user = await Users.findOneOnlineAgentById(
agentId,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
{},
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);
if (!user) {
logger.debug({
msg: 'Agent is offline. Cannot forward',
Expand Down Expand Up @@ -657,7 +662,12 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi
departmentId,
agentId,
});
const user = await Users.findOneOnlineAgentById(agentId, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
const user = await Users.findOneOnlineAgentById(
agentId,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
{},
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);
if (!user) {
throw new Error('error-user-is-offline');
}
Expand Down Expand Up @@ -727,7 +737,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi
}

const { servedBy, chatQueued } = roomTaken;
if (!chatQueued && oldServedBy && oldServedBy._id === servedBy?._id) {
if (!chatQueued && oldServedBy && servedBy && oldServedBy._id === servedBy._id) {
if (!department?.fallbackForwardDepartment?.length) {
logger.debug({
msg: 'Cannot forward room. Chat assigned to agent instead',
Expand Down
7 changes: 6 additions & 1 deletion apps/meteor/app/livechat/server/lib/QueueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,12 @@ export class QueueManager {

let defaultAgent: SelectedAgent | undefined;
const isAgentAvailable = (username: string) =>
Users.findOneOnlineAgentByUserList(username, { projection: { _id: 1 } }, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
Users.findOneOnlineAgentByUserList(
username,
{ projection: { _id: 1 } },
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);

if (servedBy?.username && (await isAgentAvailable(servedBy.username))) {
defaultAgent = { agentId: servedBy._id, username: servedBy.username };
Expand Down
7 changes: 6 additions & 1 deletion apps/meteor/app/livechat/server/lib/RoutingManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,12 @@ export const RoutingManager: Routing = {
if (
!agent ||
(agent.username &&
!(await Users.findOneOnlineAgentByUserList(agent.username, {}, settings.get<boolean>('Livechat_enabled_when_agent_idle'))) &&
!(await Users.findOneOnlineAgentByUserList(
agent.username,
{},
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
)) &&
!(await allowAgentSkipQueue(agent)))
) {
logger.debug({ msg: 'Agent offline or invalid. Using routing method to get next agent', inquiryId: inquiry._id });
Expand Down
7 changes: 6 additions & 1 deletion apps/meteor/app/livechat/server/lib/departmentsLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export async function checkOnlineForDepartment(departmentId: string) {
depUsers.map((agent) => agent.username),
{ projection: { _id: 1 } },
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);

return !!onlineForDep;
Expand All @@ -304,5 +305,9 @@ export async function getOnlineForDepartment(departmentId: string) {
const agents = await LivechatDepartmentAgents.findByDepartmentId(departmentId, { projection: { username: 1 } }).toArray();
const usernames = agents.map(({ username }) => username);

return Users.findOnlineUserFromList<ILivechatAgent>([...new Set(usernames)], settings.get<boolean>('Livechat_enabled_when_agent_idle'));
return Users.findOnlineUserFromList<ILivechatAgent>(
[...new Set(usernames)],
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,16 @@ class AutoSelection implements IRoutingMethod {
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
ignoreAgentId,
extraQuery,
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);
}

return Users.getNextAgent(ignoreAgentId, extraQuery, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
return Users.getNextAgent(
ignoreAgentId,
extraQuery,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);
}
}

Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/livechat/server/lib/routing/External.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class ExternalQueue implements IRoutingMethod {
result.username,
{},
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);

if (!agent?.username) {
Expand Down
24 changes: 20 additions & 4 deletions apps/meteor/app/livechat/server/lib/service-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ import { settings } from '../../../settings/server';

export async function getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise<FindCursor<ILivechatAgent> | undefined> {
if (agent?.agentId) {
return Users.findOnlineAgents(agent.agentId, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
return Users.findOnlineAgents(
agent.agentId,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);
}

if (department) {
return getOnlineForDepartment(department);
}
return Users.findOnlineAgents(undefined, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
return Users.findOnlineAgents(
undefined,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);
}

export async function online(department?: string, skipNoAgentSetting = false, skipFallbackCheck = false): Promise<boolean> {
Expand Down Expand Up @@ -43,7 +51,11 @@ export async function online(department?: string, skipNoAgentSetting = false, sk

export async function checkOnlineAgents(department?: string, agent?: { agentId: string }, skipFallbackCheck = false): Promise<boolean> {
if (agent?.agentId) {
return Users.checkOnlineAgents(agent.agentId, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
return Users.checkOnlineAgents(
agent.agentId,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);
}

if (department) {
Expand All @@ -62,7 +74,11 @@ export async function checkOnlineAgents(department?: string, agent?: { agentId:
return checkOnlineAgents(dep?.fallbackForwardDepartment);
}

return Users.checkOnlineAgents(undefined, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
return Users.checkOnlineAgents(
undefined,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);
}

async function countBotAgents(department?: string) {
Expand Down
8 changes: 7 additions & 1 deletion apps/meteor/app/livechat/server/lib/takeInquiry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ export const takeInquiry = async (
});
}

const user = await Users.findOneOnlineAgentById(userId, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
const user = await Users.findOneOnlineAgentById(
userId,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
{},
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);
if (!user) {
throw new Meteor.Error('error-agent-status-service-offline', 'Agent status is offline or Omnichannel service is not active', {
method: 'livechat:takeInquiry',
...(process.env.TEST_MODE && {
Livechat_enabled_when_agent_idle: settings.get<boolean>('Livechat_enabled_when_agent_idle'),
Livechat_accept_chats_with_no_agents: settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
user: await Users.findOneById(userId),
}),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@ const getDefaultAgent = async ({ username, id }: { username?: string; id?: strin
}

if (id) {
const agent = await Users.findOneOnlineAgentById(id, settings.get<boolean>('Livechat_enabled_when_agent_idle'), {
projection: { _id: 1, username: 1 },
});
const agent = await Users.findOneOnlineAgentById(
id,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
{
projection: { _id: 1, username: 1 },
},
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
);
if (agent) {
return normalizeDefaultAgent(agent);
}
Expand All @@ -43,6 +48,7 @@ const getDefaultAgent = async ({ username, id }: { username?: string; id?: strin
username || [],
{ projection: { _id: 1, username: 1 } },
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
),
);
};
Expand Down Expand Up @@ -131,6 +137,7 @@ checkDefaultAgentOnNewRoom.patch(async (_next, defaultAgent, { visitorId, source
usernameByRoom,
{ projection: { _id: 1, username: 1 } },
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
),
);
return lastRoomAgent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ class LoadBalancing {

async getNextAgent(department?: string, ignoreAgentId?: string) {
const enabledWhenIdle = settings.get<boolean>('Livechat_enabled_when_agent_idle');
const enabledWhenOffline = settings.get<boolean>('Livechat_accept_chats_with_no_agents');

const extraQuery = await getChatLimitsQuery(department);
const unavailableUsers = await Users.getUnavailableAgents(department, extraQuery, enabledWhenIdle);
logger.debug({ msg: 'Ignoring unavailable agents from assignment', unavailableUsers, department, enabledWhenIdle });
const unavailableUsers = await Users.getUnavailableAgents(department, extraQuery, enabledWhenIdle, enabledWhenOffline);
logger.debug({ msg: 'Ignoring unavailable agents from assignment', unavailableUsers, department, enabledWhenIdle, enabledWhenOffline });

const nextAgent = await Users.getNextLeastBusyAgent(
department,
ignoreAgentId,
enabledWhenIdle,
unavailableUsers.map((u) => u.username),
enabledWhenOffline,
);
if (!nextAgent) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,18 @@ class LoadRotation {

public async getNextAgent(department?: string, ignoreAgentId?: string): Promise<IOmnichannelCustomAgent | undefined> {
const enabledWhenIdle = settings.get<boolean>('Livechat_enabled_when_agent_idle');
const enabledWhenOffline = settings.get<boolean>('Livechat_accept_chats_with_no_agents');

const extraQuery = await getChatLimitsQuery(department);
const unavailableUsers = await Users.getUnavailableAgents(department, extraQuery, enabledWhenIdle);
logger.debug({ msg: 'Ignoring unavailable agents from assignment', unavailableUsers, department, enabledWhenIdle });
const unavailableUsers = await Users.getUnavailableAgents(department, extraQuery, enabledWhenIdle, enabledWhenOffline);
logger.debug({ msg: 'Ignoring unavailable agents from assignment', unavailableUsers, department, enabledWhenIdle, enabledWhenOffline });

const nextAgent = await Users.getLastAvailableAgentRouted(
department,
ignoreAgentId,
enabledWhenIdle,
unavailableUsers.map((user) => user.username),
enabledWhenOffline,
);
if (!nextAgent?.username) {
return;
Expand Down
4 changes: 3 additions & 1 deletion apps/meteor/ee/server/models/raw/Users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare module '@rocket.chat/model-typings' {
departmentId: string,
customFilter: Filter<AvailableAgentsAggregation>,
enabledWhenIdle?: boolean,
enabledWhenOffline?: boolean,
): Promise<Pick<AvailableAgentsAggregation, 'username'>[]>;
}
}
Expand All @@ -23,6 +24,7 @@ export class UsersEE extends UsersRaw {
departmentId: string,
customFilter: Filter<AvailableAgentsAggregation>,
enabledWhenIdle = false,
enabledWhenOffline = false,
): Promise<Pick<AvailableAgentsAggregation, 'username'>[]> {
// if department is provided, remove the agents that are not from the selected department
const departmentFilter = departmentId
Expand Down Expand Up @@ -53,7 +55,7 @@ export class UsersEE extends UsersRaw {
.aggregate<AvailableAgentsAggregation>(
[
{
$match: queryStatusAgentOnline({}, enabledWhenIdle),
$match: queryStatusAgentOnline({}, enabledWhenIdle, enabledWhenOffline),
},
...departmentFilter,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ async function getGuestByEmail(email: string, name: string, department = ''): Pr
email,
department,
},
{ shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle') },
{
shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle'),
shouldConsiderOfflineAgent: settings.get<boolean>('Livechat_accept_chats_with_no_agents'),
},
);

if (!livechatVisitor) {
Expand Down
4 changes: 3 additions & 1 deletion apps/meteor/tests/data/livechat/department.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@ export const createDepartmentWithAnAwayAgent = async ({
credentials: Credentials;
user: WithRequiredProperty<IUser, 'username'>;
};
ws: WebSocket;
}> => {
const { user, credentials } = await createAnAwayAgent();
const { user, credentials, ws } = await createAnAwayAgent();

const department = (await createDepartment({
allowReceiveForwardOffline,
Expand All @@ -192,6 +193,7 @@ export const createDepartmentWithAnAwayAgent = async ({
credentials,
user,
},
ws,
};
};

Expand Down
Loading
Loading