Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion apps/backend/src/handlers/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { buildImageUrl } from '../utils/image';
interface HandleAgentMessageInput extends AgentRequest {
userId: string;
projectId: string | undefined;
isProjectAdmin: boolean;
}

interface HandleAgentMessageResult {
Expand All @@ -25,14 +26,19 @@ interface HandleAgentMessageResult {
}

export const handleAgentRoute = async (opts: HandleAgentMessageInput): Promise<HandleAgentMessageResult> => {
const { userId, message, messageToEditId, model, mentions, projectId, adminMode } = opts;
const { userId, message, messageToEditId, model, mentions, projectId, isProjectAdmin } = opts;

if (!projectId) {
throw new HandlerError('BAD_REQUEST', noProjectMessage());
}

await agentService.assertBudget(projectId, model);

let adminMode = opts.adminMode ?? false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unpack the adminMode if used twice

if (opts.adminMode === undefined && isProjectAdmin && opts.chatId) {
adminMode = await chatQueries.wasLastUserMessageAdmin(opts.chatId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Follow-up admin messages with omitted adminMode now run against admin data but are recorded as web in MessageSent. Use resolved mode for telemetry so admin-mode usage and normal warehouse usage remain distinguishable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/src/handlers/agent.ts, line 39:

<comment>Follow-up admin messages with omitted `adminMode` now run against admin data but are recorded as `web` in `MessageSent`. Use resolved mode for telemetry so admin-mode usage and normal warehouse usage remain distinguishable.</comment>

<file context>
@@ -25,14 +26,19 @@ interface HandleAgentMessageResult {
 
+	let adminMode = opts.adminMode ?? false;
+	if (opts.adminMode === undefined && isProjectAdmin && opts.chatId) {
+		adminMode = await chatQueries.wasLastUserMessageAdmin(opts.chatId);
+	}
+
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if last user message or first user message should be considered, to me if the first message is source=admin then all the messages in the convo should be but the issue with this is that it forces it under the hood even if the user deactivate the admin mode in the frontend

@ad4mou ad4mou Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes actually that's what caused the issue.
I think it makes more sense to just follow the most recent message but if the user changes manually the mode, then that takes priority and hence we change the mode.

Whether that should be the first message or the last, I think most recent makes more sense
But it's true in a practical scenario the user should stick to one mode in one chat. So perhaps we could lock the mode after the first prompt in a chat

}

const source: MessageSource = adminMode ? 'admin' : 'web';
let chatId = opts.chatId;
const isNewChat = !chatId;
Expand Down
12 changes: 12 additions & 0 deletions apps/backend/src/queries/chat.queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,18 @@ export const getChatOwnerId = async (chatId: string): Promise<string | undefined
return result?.userId;
};

export const wasLastUserMessageAdmin = async (chatId: string): Promise<boolean> => {
const [row] = await db
.select({ source: s.chatMessage.source })
.from(s.chatMessage)
.where(
and(eq(s.chatMessage.chatId, chatId), eq(s.chatMessage.role, 'user'), isNull(s.chatMessage.supersededAt)),
)
.orderBy(desc(s.chatMessage.createdAt))
.limit(1);
return row?.source === 'admin';
};

/** Marks all messages from a given message id onwards as superseeded (won't be used in the conversation anymore). */
export const supersedeMessagesFrom = async (chatId: string, fromMessageId: string): Promise<void> => {
await db.transaction(async (t) => {
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/routes/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const agentRoutes = async (app: App) => {
projectId,
...body,
adminMode: body.adminMode && isProjectAdmin,
isProjectAdmin,
});

posthog.capture(user.id, PostHogEvent.MessageSent, {
Expand Down
18 changes: 9 additions & 9 deletions apps/frontend/src/hooks/use-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export const useAgent = ({ disableNavigation = false }: { disableNavigation?: bo
model: activeSelectedModelRef.current ?? undefined,
mentions: mentions.length > 0 ? mentions : undefined,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
adminMode: adminModeAtSend || undefined,
adminMode: adminModeAtSend,
},
};
},
Expand Down Expand Up @@ -257,9 +257,9 @@ export const useAgent = ({ disableNavigation = false }: { disableNavigation?: bo
}
}, [error]); // eslint-disable-line react-hooks/exhaustive-deps

// Carry admin mode across an admin conversation: enable it for chats whose first or
// last user message was sent from admin mode, and disable it for normal chats. A
// freshly created chat keeps its current mode so follow-ups stay in admin mode.
// Carry admin mode across an admin conversation: on load, start in the mode of the latest
// user turn (derived from server data, which carries `source`). Once loaded, a manual toggle
// always wins — this effect syncs a chat only once, so it never overrides a later user toggle.
const syncedAdminChatRef = useRef<string | undefined>(undefined);
useEffect(() => {
if (!chatId) {
Expand All @@ -271,14 +271,14 @@ export const useAgent = ({ disableNavigation = false }: { disableNavigation?: bo
syncedAdminChatRef.current = chatId;
return;
}
if (chat.isLoading || messages.length === 0 || syncedAdminChatRef.current === chatId) {
const serverMessages = chat.data?.messages;
if (chat.isLoading || !serverMessages || serverMessages.length === 0 || syncedAdminChatRef.current === chatId) {
return;
}
syncedAdminChatRef.current = chatId;
const userMessages = messages.filter((m) => m.role === 'user');
const conversationIsAdmin = userMessages.at(0)?.source === 'admin' || userMessages.at(-1)?.source === 'admin';
setAdminMode(conversationIsAdmin);
}, [chatId, chat.isLoading, messages, setAdminMode]);
const lastUserMessage = serverMessages.filter((m) => m.role === 'user').at(-1);
setAdminMode(lastUserMessage?.source === 'admin');
}, [chatId, chat.isLoading, chat.data?.messages, setAdminMode]);

const stopAgent = useCallback(async () => {
if (!chatId) {
Expand Down
Loading