diff --git a/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-chat-session-read-state.repository.ts b/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-chat-session-read-state.repository.ts index 6679bf64..4c5c6faa 100644 --- a/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-chat-session-read-state.repository.ts +++ b/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-chat-session-read-state.repository.ts @@ -4,6 +4,14 @@ import { In, Repository } from 'typeorm'; import { UserChatSessionReadStateEntity } from '../entities/user-chat-session-read-state.entity'; +function laterDate(a: Date | null | undefined, b: Date): Date { + if (!a) { + return b; + } + + return a.getTime() > b.getTime() ? a : b; +} + @Injectable() export class UserChatSessionReadStateRepository { constructor( @@ -20,6 +28,11 @@ export class UserChatSessionReadStateRepository { return await this.repository.findOne({ where: { userId, clientId, agentId, chatSessionId } }); } + /** + * Upsert read cursor. Never moves `lastReadAt` backwards. + * Omit `lastReadAgentMessageId` (undefined) to keep the existing message id; + * pass null only when intentionally clearing. + */ async upsertReadState(params: { userId: string; clientId: string; @@ -31,8 +44,11 @@ export class UserChatSessionReadStateRepository { const existing = await this.findOne(params.userId, params.clientId, params.agentId, params.chatSessionId); if (existing) { - existing.lastReadAt = params.lastReadAt; - existing.lastReadAgentMessageId = params.lastReadAgentMessageId ?? null; + existing.lastReadAt = laterDate(existing.lastReadAt, params.lastReadAt); + + if (params.lastReadAgentMessageId !== undefined) { + existing.lastReadAgentMessageId = params.lastReadAgentMessageId; + } return await this.repository.save(existing); } diff --git a/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-environment-read-state.repository.spec.ts b/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-environment-read-state.repository.spec.ts index 82b754a8..b2468ac2 100644 --- a/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-environment-read-state.repository.spec.ts +++ b/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-environment-read-state.repository.spec.ts @@ -80,6 +80,27 @@ describe('UserEnvironmentReadStateRepository', () => { expect(mockTypeOrmRepository.create).not.toHaveBeenCalled(); }); + it('does not move lastReadAt backwards or clear message id when omitted', async () => { + const existing = { + ...mockRow, + lastReadAt: new Date('2026-02-01T00:00:00.000Z'), + lastReadAgentMessageId: 'msg-keep', + }; + + mockTypeOrmRepository.findOne.mockResolvedValue(existing); + mockTypeOrmRepository.save.mockImplementation(async (row) => row); + + const result = await repository.upsertReadState({ + userId: 'user-1', + clientId: 'client-1', + agentId: 'agent-1', + lastReadAt: new Date('2026-01-01T00:00:00.000Z'), + }); + + expect(result.lastReadAt).toEqual(new Date('2026-02-01T00:00:00.000Z')); + expect(result.lastReadAgentMessageId).toBe('msg-keep'); + }); + it('creates row when missing', async () => { mockTypeOrmRepository.findOne.mockResolvedValue(null); mockTypeOrmRepository.create.mockReturnValue(mockRow); diff --git a/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-environment-read-state.repository.ts b/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-environment-read-state.repository.ts index defdbb48..86f78bd2 100644 --- a/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-environment-read-state.repository.ts +++ b/libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/user-environment-read-state.repository.ts @@ -29,8 +29,13 @@ export class UserEnvironmentReadStateRepository { const existing = await this.findOne(params.userId, params.clientId, params.agentId); if (existing) { - existing.lastReadAt = params.lastReadAt; - existing.lastReadAgentMessageId = params.lastReadAgentMessageId ?? null; + const existingAt = existing.lastReadAt; + existing.lastReadAt = + existingAt && existingAt.getTime() > params.lastReadAt.getTime() ? existingAt : params.lastReadAt; + + if (params.lastReadAgentMessageId !== undefined) { + existing.lastReadAgentMessageId = params.lastReadAgentMessageId; + } return await this.repository.save(existing); } diff --git a/libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/agent-console-status.service.spec.ts b/libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/agent-console-status.service.spec.ts index 536a3851..7150756f 100644 --- a/libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/agent-console-status.service.spec.ts +++ b/libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/agent-console-status.service.spec.ts @@ -212,6 +212,62 @@ describe('AgentConsoleStatusService', () => { expect(realtime.emitToUser).toHaveBeenCalledWith('user-1', 'statusPatch', expect.any(Object)); }); + it('computes unread after VCS so a mark-read during build is visible', async () => { + const messageCreatedAt = new Date('2026-01-02T00:00:00.000Z'); + messagesProxy.getLatestAgentMessage.mockResolvedValue({ + id: 'msg-1', + createdAt: messageCreatedAt.toISOString(), + }); + + let vcsResolve: ((value: unknown) => void) | undefined; + const vcsGate = new Promise((resolve) => { + vcsResolve = resolve; + }); + + vcsProxy.getStatus.mockImplementation(async () => { + await vcsGate; + + return { + isClean: true, + hasUnpushedCommits: false, + files: [], + }; + }); + + const snapshotPromise = service.buildSnapshotForUser({ + isApiKeyAuth: false, + userId: 'user-1', + userRole: UserRole.USER, + user: { id: 'user-1', roles: [] }, + }); + + // Let phase-1 reach the gated VCS call. + await Promise.resolve(); + await Promise.resolve(); + + // Mark-read lands while VCS is still in flight; phase-2 must observe it. + chatReadStateRepository.findByUserAndClientIds.mockResolvedValue([ + { + userId: 'user-1', + clientId: 'client-1', + agentId: 'agent-1', + chatSessionId: USER_CHAT_ID, + lastReadAt: new Date('2026-01-03T00:00:00.000Z'), + }, + ]); + + vcsResolve?.({ + isClean: true, + hasUnpushedCommits: false, + files: [], + }); + + const snapshot = await snapshotPromise; + const chats = snapshot.environments[0].chats ?? []; + + expect(chats.find((c) => c.chatSessionId === USER_CHAT_ID)?.hasUnreadMessages).toBe(false); + }); + it('notifyVcsStateChanged emits status patches to users with client access', async () => { vcsProxy.getStatus.mockResolvedValue({ isClean: true, diff --git a/libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/agent-console-status.service.ts b/libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/agent-console-status.service.ts index 753fa2af..47704d1b 100644 --- a/libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/agent-console-status.service.ts +++ b/libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/agent-console-status.service.ts @@ -152,12 +152,14 @@ export class AgentConsoleStatusService { const resolvedChatId = chatSessionId ?? (await this.resolvePrimaryChatId(clientId, agentId)); const latest = await this.messagesProxy.getLatestAgentMessage(clientId, agentId, resolvedChatId ?? undefined); + const latestAt = latest?.createdAt ? new Date(latest.createdAt) : null; + const readAt = this.maxDate(new Date(), latestAt) ?? new Date(); await this.readStateRepository.upsertReadState({ userId, clientId, agentId, - lastReadAt: new Date(), + lastReadAt: readAt, lastReadAgentMessageId: latest?.id ?? null, }); @@ -167,7 +169,7 @@ export class AgentConsoleStatusService { clientId, agentId, chatSessionId: resolvedChatId, - lastReadAt: new Date(), + lastReadAt: readAt, lastReadAgentMessageId: latest?.id ?? null, }); } @@ -190,13 +192,15 @@ export class AgentConsoleStatusService { await this.assertEnvironmentAccess(userInfo, clientId, agentId); const latest = await this.messagesProxy.getLatestAgentMessage(clientId, agentId, chatSessionId); + const latestAt = latest?.createdAt ? new Date(latest.createdAt) : null; + const readAt = this.maxDate(new Date(), latestAt) ?? new Date(); await this.chatReadStateRepository.upsertReadState({ userId, clientId, agentId, chatSessionId, - lastReadAt: new Date(), + lastReadAt: readAt, lastReadAgentMessageId: latest?.id ?? null, }); @@ -204,7 +208,7 @@ export class AgentConsoleStatusService { userId, clientId, agentId, - lastReadAt: new Date(), + lastReadAt: readAt, lastReadAgentMessageId: latest?.id ?? null, }); @@ -302,6 +306,11 @@ export class AgentConsoleStatusService { activityAt: Date, ): Promise { const userIds = await this.resolveUserIdsToNotify(clientId); + const latest = chatSessionId + ? await this.messagesProxy.getLatestAgentMessage(clientId, agentId, chatSessionId) + : await this.messagesProxy.getLatestAgentMessage(clientId, agentId); + const latestAt = latest?.createdAt ? new Date(latest.createdAt) : null; + const readAt = this.maxDate(activityAt, latestAt) ?? activityAt; for (const userId of userIds) { const activeForUser = this.findActiveEnvironmentForUser(userId, clientId, agentId, chatSessionId); @@ -311,8 +320,9 @@ export class AgentConsoleStatusService { userId, clientId, agentId, - lastReadAt: activityAt, - lastReadAgentMessageId: null, + lastReadAt: readAt, + // Keep prior message id when latest is unknown — never wipe a successful mark-read. + ...(latest?.id ? { lastReadAgentMessageId: latest.id } : {}), }); if (chatSessionId) { @@ -321,8 +331,8 @@ export class AgentConsoleStatusService { clientId, agentId, chatSessionId, - lastReadAt: activityAt, - lastReadAgentMessageId: null, + lastReadAt: readAt, + ...(latest?.id ? { lastReadAgentMessageId: latest.id } : {}), }); } } @@ -401,16 +411,21 @@ export class AgentConsoleStatusService { } private async buildEnvironmentsForUser(userId: string, clientIds: string[]): Promise { - const chatReadStates = await this.chatReadStateRepository.findByUserAndClientIds(userId, clientIds); - const chatReadByKey = new Map(); - - for (const row of chatReadStates) { - chatReadByKey.set(`${row.clientId}:${row.agentId}:${row.chatSessionId}`, row); - } + type PendingEnv = { + clientId: string; + agentId: string; + gitDirty: boolean; + gitConflict: boolean; + visibleChats: Array<{ id: string; kind: string }>; + primaryChatId: string | null; + latestAutomationAt: Date | null; + }; - const environments: EnvironmentStatusPayload[] = []; + const pending: PendingEnv[] = []; const vcsConcurrency = this.getVcsConcurrency(); + // Phase 1: resolve agents + slow VCS. Unread is computed afterward so a mark-read + // that lands during VCS cannot be overwritten by a stale pre-mark read snapshot. for (const clientId of clientIds) { let agents: AgentResponseDto[] = []; @@ -428,30 +443,11 @@ export class AgentConsoleStatusService { const chunk = agents.slice(i, i + vcsConcurrency); const chunkResults = await Promise.all( chunk.map(async (agent) => { - const visibleChats = (agent.chats ?? []).filter((c) => c.kind === 'primary' || c.kind === 'user'); + const visibleChats = (agent.chats ?? []) + .filter((c) => c.kind === 'primary' || c.kind === 'user') + .map((c) => ({ id: c.id, kind: c.kind })); const primaryChatId = agent.primaryChatId ?? visibleChats.find((c) => c.kind === 'primary')?.id ?? null; const latestAutomationAt = automationByAgent.get(agent.id) ?? null; - - const chatStatuses: ChatSessionStatusPayload[] = await Promise.all( - visibleChats.map(async (chat) => { - const latestAgentMsg = await this.messagesProxy.getLatestAgentMessage(clientId, agent.id, chat.id); - const latestAgentAt = latestAgentMsg ? new Date(latestAgentMsg.createdAt) : null; - const activityAt = - chat.kind === 'primary' || chat.id === primaryChatId - ? this.maxDate(latestAgentAt, latestAutomationAt) - : latestAgentAt; - const readState = chatReadByKey.get(`${clientId}:${agent.id}:${chat.id}`); - const lastReadAt = readState?.lastReadAt ?? null; - const hasUnreadMessages = activityAt !== null && (lastReadAt === null || activityAt > lastReadAt); - - return { - chatSessionId: chat.id, - hasUnreadMessages, - }; - }), - ); - - const hasUnreadMessages = chatStatuses.some((c) => c.hasUnreadMessages); let gitDirty = false; let gitConflict = false; @@ -468,19 +464,72 @@ export class AgentConsoleStatusService { return { clientId, agentId: agent.id, - hasUnreadMessages, gitDirty, gitConflict, - chats: chatStatuses, - } satisfies EnvironmentStatusPayload; + visibleChats, + primaryChatId, + latestAutomationAt, + } satisfies PendingEnv; }), ); - environments.push(...chunkResults); + pending.push(...chunkResults); } } - return environments; + // Phase 2: fresh read cursors + latest messages (after any concurrent mark-read). + const chatReadStates = await this.chatReadStateRepository.findByUserAndClientIds(userId, clientIds); + const chatReadByKey = new Map(); + + for (const row of chatReadStates) { + chatReadByKey.set(`${row.clientId}:${row.agentId}:${row.chatSessionId}`, row); + } + + return await Promise.all( + pending.map(async (item) => { + const chatStatuses: ChatSessionStatusPayload[] = await Promise.all( + item.visibleChats.map(async (chat) => { + const latestAgentMsg = await this.messagesProxy.getLatestAgentMessage(item.clientId, item.agentId, chat.id); + const latestAgentAt = latestAgentMsg ? new Date(latestAgentMsg.createdAt) : null; + const isPrimary = chat.kind === 'primary' || chat.id === item.primaryChatId; + const readState = chatReadByKey.get(`${item.clientId}:${item.agentId}:${chat.id}`); + const lastReadAt = readState?.lastReadAt ?? null; + const lastReadMessageId = readState?.lastReadAgentMessageId ?? null; + + // Prefer message-id equality so clock skew / auto-clear races cannot revive unread. + let messageUnread = false; + + if (latestAgentMsg) { + if (lastReadMessageId && lastReadMessageId === latestAgentMsg.id) { + messageUnread = false; + } else { + messageUnread = lastReadAt === null || (latestAgentAt !== null && latestAgentAt > lastReadAt); + } + } + + const automationUnread = + isPrimary && + item.latestAutomationAt !== null && + (lastReadAt === null || item.latestAutomationAt > lastReadAt); + const hasUnreadMessages = messageUnread || Boolean(automationUnread); + + return { + chatSessionId: chat.id, + hasUnreadMessages, + }; + }), + ); + + return { + clientId: item.clientId, + agentId: item.agentId, + hasUnreadMessages: chatStatuses.some((c) => c.hasUnreadMessages), + gitDirty: item.gitDirty, + gitConflict: item.gitConflict, + chats: chatStatuses, + } satisfies EnvironmentStatusPayload; + }), + ); } private async buildEnvironmentStatus( diff --git a/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.effects.ts b/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.effects.ts index 0eb8f2e0..9881e89f 100644 --- a/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.effects.ts +++ b/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.effects.ts @@ -64,11 +64,43 @@ function getAuthHeader(environment: Environment, keycloakService: KeycloakServic let statusSocketInstance: Socket | null = null; let notificationAudio: HTMLAudioElement | null = null; +let pendingActiveEnvironment: { + clientId: string | null; + agentId: string | null; + chatSessionId?: string | null; +} | null = null; +let pendingChatSessionRead: { clientId: string; agentId: string; chatSessionId: string } | null = null; +let pendingEnvironmentRead: { clientId: string; agentId: string; chatSessionId?: string } | null = null; export function getStatusSocketInstance(): Socket | null { return statusSocketInstance; } +/** @internal Test helper — reset module-level pending emit buffers. */ +export function resetStatusSocketPendingEmitsForTests(): void { + pendingActiveEnvironment = null; + pendingChatSessionRead = null; + pendingEnvironmentRead = null; +} + +function flushPendingStatusEmits(): void { + if (!statusSocketInstance?.connected) { + return; + } + + if (pendingActiveEnvironment) { + statusSocketInstance.emit(STATUS_SOCKET_EVENTS.setActiveEnvironment, pendingActiveEnvironment); + } + + if (pendingChatSessionRead) { + statusSocketInstance.emit(STATUS_SOCKET_EVENTS.markChatSessionRead, pendingChatSessionRead); + } + + if (pendingEnvironmentRead) { + statusSocketInstance.emit(STATUS_SOCKET_EVENTS.markEnvironmentRead, pendingEnvironmentRead); + } +} + function playNotificationSound(): void { if (typeof Audio === 'undefined') { return; @@ -117,6 +149,7 @@ export const connectNotificationsSocket$ = createEffect( }); const connectSuccess$ = fromEvent(statusSocketInstance, 'connect').pipe( + tap(() => flushPendingStatusEmits()), map(() => connectNotificationsSocketSuccess()), ); const connectError$ = fromEvent(statusSocketInstance, 'connect_error').pipe( @@ -127,6 +160,7 @@ export const connectNotificationsSocket$ = createEffect( fromEvent(statusSocketInstance, 'reconnecting'), ).pipe(map((attempt) => notificationsSocketReconnecting({ attempt }))); const reconnect$ = fromEvent(statusSocketInstance, 'reconnect').pipe( + tap(() => flushPendingStatusEmits()), map(() => notificationsSocketReconnected()), ); const reconnectError$ = fromEvent(statusSocketInstance, 'reconnect_error').pipe( @@ -188,11 +222,11 @@ export const markEnvironmentRead$ = createEffect( actions$.pipe( ofType(markEnvironmentRead), tap(({ clientId, agentId, chatSessionId }) => { - statusSocketInstance?.emit(STATUS_SOCKET_EVENTS.markEnvironmentRead, { - clientId, - agentId, - chatSessionId, - }); + pendingEnvironmentRead = { clientId, agentId, chatSessionId }; + + if (statusSocketInstance?.connected) { + statusSocketInstance.emit(STATUS_SOCKET_EVENTS.markEnvironmentRead, pendingEnvironmentRead); + } }), ), { functional: true, dispatch: false }, @@ -203,11 +237,11 @@ export const markChatSessionRead$ = createEffect( actions$.pipe( ofType(markChatSessionRead), tap(({ clientId, agentId, chatSessionId }) => { - statusSocketInstance?.emit(STATUS_SOCKET_EVENTS.markChatSessionRead, { - clientId, - agentId, - chatSessionId, - }); + pendingChatSessionRead = { clientId, agentId, chatSessionId }; + + if (statusSocketInstance?.connected) { + statusSocketInstance.emit(STATUS_SOCKET_EVENTS.markChatSessionRead, pendingChatSessionRead); + } }), ), { functional: true, dispatch: false }, @@ -218,11 +252,12 @@ export const setActiveEnvironment$ = createEffect( actions$.pipe( ofType(setActiveEnvironment), tap(({ clientId, agentId, chatSessionId }) => { - statusSocketInstance?.emit(STATUS_SOCKET_EVENTS.setActiveEnvironment, { - clientId, - agentId, - chatSessionId, - }); + pendingActiveEnvironment = { clientId, agentId, chatSessionId }; + + if (statusSocketInstance?.connected) { + statusSocketInstance.emit(STATUS_SOCKET_EVENTS.setActiveEnvironment, pendingActiveEnvironment); + } + store.dispatch( setActiveEnvironmentLocal({ active: clientId && agentId ? { clientId, agentId, chatSessionId: chatSessionId ?? null } : null, @@ -233,6 +268,35 @@ export const setActiveEnvironment$ = createEffect( { functional: true, dispatch: false }, ); +/** After snapshot, re-assert active chat + mark-read so reload races cannot drop the cursor. */ +export const reassertActiveChatAfterSnapshot$ = createEffect( + (actions$ = inject(Actions), store = inject(Store)) => + actions$.pipe( + ofType(statusSnapshotReceived), + withLatestFrom(store.select(selectActiveEnvironment)), + tap(([, active]) => { + if (!statusSocketInstance?.connected || !active?.clientId || !active?.agentId) { + return; + } + + statusSocketInstance.emit(STATUS_SOCKET_EVENTS.setActiveEnvironment, { + clientId: active.clientId, + agentId: active.agentId, + chatSessionId: active.chatSessionId, + }); + + if (active.chatSessionId) { + statusSocketInstance.emit(STATUS_SOCKET_EVENTS.markChatSessionRead, { + clientId: active.clientId, + agentId: active.agentId, + chatSessionId: active.chatSessionId, + }); + } + }), + ), + { functional: true, dispatch: false }, +); + export const playUnreadNotificationSound$ = createEffect( (actions$ = inject(Actions), store = inject(Store)) => actions$.pipe( diff --git a/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.reducer.spec.ts b/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.reducer.spec.ts index 94796086..dcb2bbfc 100644 --- a/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.reducer.spec.ts +++ b/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.reducer.spec.ts @@ -176,7 +176,7 @@ describe('notificationsReducer', () => { expect(stale.environmentsByKey['c1:a1']?.hasUnreadMessages).toBe(false); }); - it('accepts unread=true after server confirms read (suppress cleared)', () => { + it('keeps suppress after server confirms read so late unread=true stays blocked', () => { const withUnread = notificationsReducer( initialNotificationsState, statusSnapshotReceived({ @@ -220,9 +220,9 @@ describe('notificationsReducer', () => { }), ); - expect(confirmed.optimisticChatReadAtByKey['c1:a1:chat-1']).toBeUndefined(); + expect(confirmed.optimisticChatReadAtByKey['c1:a1:chat-1']).toBeDefined(); - const fresh = notificationsReducer( + const lateStale = notificationsReducer( confirmed, statusPatchReceived({ patch: { @@ -241,7 +241,9 @@ describe('notificationsReducer', () => { }), ); - expect(fresh.environmentsByKey['c1:a1']?.chats).toEqual([{ chatSessionId: 'chat-1', hasUnreadMessages: true }]); + expect(lateStale.environmentsByKey['c1:a1']?.chats).toEqual([ + { chatSessionId: 'chat-1', hasUnreadMessages: false }, + ]); }); it('merges status patch for environment', () => { diff --git a/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.reducer.ts b/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.reducer.ts index 0577e97a..dbca9412 100644 --- a/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.reducer.ts +++ b/libs/domains/agenstra/frontend/data-access-agent-console/src/lib/state/notifications/notifications.reducer.ts @@ -155,11 +155,8 @@ export function mergeEnvironmentWithOptimisticReads( const suppressActive = nextOptimistic[key] != null; if (!chat.hasUnreadMessages) { - // Server confirmed read — drop suppress so a later real unread can surface. - if (suppressActive) { - delete nextOptimistic[key]; - } - + // Keep suppress until TTL so a slower in-flight unread=true patch (e.g. VCS build) + // cannot clear suppress and then resurrect the badge. return { ...chat, hasUnreadMessages: false }; } diff --git a/libs/domains/agenstra/frontend/feature-agent-console/src/lib/agent-console.routes.ts b/libs/domains/agenstra/frontend/feature-agent-console/src/lib/agent-console.routes.ts index 96ad422b..83716e51 100644 --- a/libs/domains/agenstra/frontend/feature-agent-console/src/lib/agent-console.routes.ts +++ b/libs/domains/agenstra/frontend/feature-agent-console/src/lib/agent-console.routes.ts @@ -126,6 +126,7 @@ import { loadWorkflows$, loadWorkspaceConfigurationOverrides$, markEnvironmentRead$, + markChatSessionRead$, migrateTicket$, moveFileOrDirectory$, NotificationsFacade, @@ -141,6 +142,7 @@ import { readFile$, rebase$, refreshTicketDetailActivityAfterAutomation$, + reassertActiveChatAfterSnapshot$, reloadActivityAfterWrite$, reloadRelationsAfterWrite$, reloadTreeAfterWrite$, @@ -404,7 +406,9 @@ export const agentConsoleRoutes: Route[] = [ disconnectKnowledgeBoardSocket$, disconnectNotificationsSocket$, markEnvironmentRead$, + markChatSessionRead$, setActiveEnvironment$, + reassertActiveChatAfterSnapshot$, playUnreadNotificationSound$, playUnreadSoundEffect$, restoreKnowledgeBoardSocketClient$,