@@ -65,7 +65,7 @@ import {
6565} from "~/services/dashboardAgentQuota.server" ;
6666import { logger } from "~/services/logger.server" ;
6767import { resolveTriggerUrisInOrganization } from "~/services/resolveTriggerUriInOrganization.server" ;
68- import { requireUser } from "~/services/session.server" ;
68+ import { getImpersonatorUserId , requireUser } from "~/services/session.server" ;
6969import { EnvironmentParamSchema } from "~/utils/pathBuilder" ;
7070import { canAccessDashboardAgent } from "~/v3/canAccessDashboardAgent.server" ;
7171import { canUseDashboardAgentWatches } from "~/v3/canUseDashboardAgentWatches.server" ;
@@ -112,6 +112,7 @@ const ActionBody = z.object({
112112export const loader = async ( { request, params } : LoaderFunctionArgs ) => {
113113 const user = await requireUser ( request ) ;
114114 const userId = user . id ;
115+ const actingUserId = user . isImpersonating ? await getImpersonatorUserId ( request ) : undefined ;
115116 const { organizationSlug, projectParam } = EnvironmentParamSchema . parse ( params ) ;
116117
117118 if (
@@ -142,11 +143,13 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
142143 if ( ! scoped ) return json ( { error : "Project not found" } , { status : 404 } ) ;
143144
144145 // No watches means no wakes: the feed isn't read at all when they're off.
145- const watchEnabled = await canUseDashboardAgentWatches ( {
146- userId,
147- organizationSlug,
148- orgFeatureFlags : ( scoped . organization . featureFlags as Record < string , unknown > ) ?? { } ,
149- } ) ;
146+ const watchEnabled =
147+ ! actingUserId &&
148+ ( await canUseDashboardAgentWatches ( {
149+ userId,
150+ organizationSlug,
151+ orgFeatureFlags : ( scoped . organization . featureFlags as Record < string , unknown > ) ?? { } ,
152+ } ) ) ;
150153
151154 const [ feed , unreadWork ] = await Promise . all ( [
152155 watchEnabled
@@ -160,6 +163,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
160163 countChatsWithUnreadWork ( dashboardAgentDb , {
161164 organizationId : scoped . organizationId ,
162165 userId,
166+ actingUserId,
163167 // The chat the panel has on screen, if any: it is being read as this is counted.
164168 excludeChatId : searchParams . get ( "chatId" ) ?? undefined ,
165169 } ) ,
@@ -177,7 +181,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
177181 // Free for now: tell a mounted client explicitly, so it drops any cached used/limit
178182 // and cap-reached state from before the switch flipped off, instead of keeping it
179183 // until remount (a `{}` body would silently ignore and keep the stale state).
180- if ( ! isDashboardAgentQuotaEnabled ( ) ) return json ( { enabled : false } ) ;
184+ if ( ! isDashboardAgentQuotaEnabled ( ) || actingUserId ) return json ( { enabled : false } ) ;
181185
182186 const quota = await resolveAgentMessageQuota ( dashboardAgentDb , {
183187 organizationId : project . organizationId ,
@@ -194,8 +198,18 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
194198 const chatId = searchParams . get ( "chatId" ) ;
195199 if ( chatId ) {
196200 const [ messages , session ] = await Promise . all ( [
197- getChatMessages ( dashboardAgentDb , { chatId, userId, organizationId : project . organizationId } ) ,
198- getSession ( dashboardAgentDb , { chatId, userId, organizationId : project . organizationId } ) ,
201+ getChatMessages ( dashboardAgentDb , {
202+ chatId,
203+ userId,
204+ actingUserId,
205+ organizationId : project . organizationId ,
206+ } ) ,
207+ getSession ( dashboardAgentDb , {
208+ chatId,
209+ userId,
210+ actingUserId,
211+ organizationId : project . organizationId ,
212+ } ) ,
199213 ] ) ;
200214 // Null is not an empty transcript: the chat is deleted or another org's, and a 200 would
201215 // read as a real, empty chat.
@@ -221,13 +235,16 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
221235 const chats = await listChats ( dashboardAgentDb , {
222236 organizationId : project . organizationId ,
223237 userId,
238+ actingUserId,
224239 } ) ;
225240
226- const watchEnabled = await canUseDashboardAgentWatches ( {
227- userId,
228- organizationSlug,
229- orgFeatureFlags,
230- } ) ;
241+ const watchEnabled =
242+ ! actingUserId &&
243+ ( await canUseDashboardAgentWatches ( {
244+ userId,
245+ organizationSlug,
246+ orgFeatureFlags,
247+ } ) ) ;
231248
232249 // One query each for all the listed chats, never one per row. With watches off the
233250 // history carries none, so an org that loses the flag stops seeing its old ones.
@@ -254,6 +271,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
254271 listChatIdsWithOpenInvestigations ( dashboardAgentDb , {
255272 organizationId : project . organizationId ,
256273 userId,
274+ actingUserId,
257275 } ) ,
258276 ] ) ;
259277
@@ -285,6 +303,7 @@ function messageTooLarge() {
285303export const action = async ( { request, params } : ActionFunctionArgs ) => {
286304 const user = await requireUser ( request ) ;
287305 const userId = user . id ;
306+ const actingUserId = user . isImpersonating ? await getImpersonatorUserId ( request ) : undefined ;
288307 const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema . parse ( params ) ;
289308
290309 if (
@@ -337,9 +356,11 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
337356
338357 wellFormMessageText ( firstMessage . parts ) ;
339358
340- const quota = await resolveAgentMessageQuota ( dashboardAgentDb , {
341- organizationId : project . organizationId ,
342- } ) ;
359+ const quota = actingUserId
360+ ? null
361+ : await resolveAgentMessageQuota ( dashboardAgentDb , {
362+ organizationId : project . organizationId ,
363+ } ) ;
343364 if ( quota ?. reached ) {
344365 return json ( { error : MESSAGE_QUOTA_REACHED_ERROR , limit : quota . limit } , { status : 403 } ) ;
345366 }
@@ -356,11 +377,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
356377 const clientContext = pickAgentClientMetadata ( clientData ) ;
357378 // Server-resolved, never sent by the browser: off means the turn gets no watch
358379 // tools and no watch guidance.
359- const watchEnabled = await canUseDashboardAgentWatches ( {
360- userId,
361- organizationSlug,
362- orgFeatureFlags,
363- } ) ;
380+ const watchEnabled =
381+ ! actingUserId &&
382+ ( await canUseDashboardAgentWatches ( {
383+ userId,
384+ organizationSlug,
385+ orgFeatureFlags,
386+ } ) ) ;
364387
365388 // Membership-scoped: dev rows are per-developer, so a token must never be minted for
366389 // someone else's environment — or, when nothing resolves, for no environment at all.
@@ -411,6 +434,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
411434 id : chatId ,
412435 organizationId : project . organizationId ,
413436 userId,
437+ ...( actingUserId ? { createdByUserId : actingUserId } : { } ) ,
414438 ...( clientData ? { metadata : { context : clientContext } } : { } ) ,
415439 } ) ;
416440
@@ -452,6 +476,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
452476 await softDeleteChat ( dashboardAgentDb , {
453477 chatId,
454478 userId,
479+ actingUserId,
455480 organizationId : project . organizationId ,
456481 } ) . catch ( ( cleanupError ) => {
457482 logger . error ( "Failed to remove a dashboard agent chat whose start failed" , {
@@ -465,7 +490,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
465490
466491 // Only the head start dispatches the first message here; a cold start sends it through
467492 // the `in` proxy, which counts it there. Counting both would double-count.
468- if ( headStarted ) {
493+ if ( headStarted && ! actingUserId ) {
469494 await recordAgentMessageSent ( dashboardAgentDb , {
470495 organizationId : project . organizationId ,
471496 } ) ;
@@ -557,7 +582,10 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
557582 // The configuration card's submit path. The environment comes from the URL and goes
558583 // through the same re-authorization a background tick passes, never from the body.
559584 if ( parsed . data . intent === "watch-create" ) {
560- if ( ! ( await canUseDashboardAgentWatches ( { userId, organizationSlug, orgFeatureFlags } ) ) ) {
585+ if (
586+ actingUserId ||
587+ ! ( await canUseDashboardAgentWatches ( { userId, organizationSlug, orgFeatureFlags } ) )
588+ ) {
561589 return json ( { error : "Not found" } , { status : 404 } ) ;
562590 }
563591
@@ -597,6 +625,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
597625 ! ( await chatExists ( dashboardAgentDb , {
598626 chatId : targetChatId ,
599627 userId,
628+ actingUserId,
600629 organizationId : project . organizationId ,
601630 } ) )
602631 ) {
@@ -646,6 +675,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
646675 ! ( await chatExists ( dashboardAgentDb , {
647676 chatId,
648677 userId,
678+ actingUserId,
649679 organizationId : project . organizationId ,
650680 } ) )
651681 ) {
@@ -679,11 +709,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
679709 ...pickAgentClientMetadata ( clientData ) ,
680710 // Server-resolved, like every other field here: the resumed run's first turn
681711 // gets no watch tools while the flag is off.
682- watchEnabled : await canUseDashboardAgentWatches ( {
683- userId,
684- organizationSlug,
685- orgFeatureFlags,
686- } ) ,
712+ watchEnabled :
713+ ! actingUserId &&
714+ ( await canUseDashboardAgentWatches ( {
715+ userId,
716+ organizationSlug,
717+ orgFeatureFlags,
718+ } ) ) ,
687719 organizationId : project . organizationId ,
688720 userId,
689721 projectId : project . id ,
@@ -728,6 +760,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
728760 ! ( await chatExists ( dashboardAgentDb , {
729761 chatId,
730762 userId,
763+ actingUserId,
731764 organizationId : project . organizationId ,
732765 } ) )
733766 ) {
@@ -741,6 +774,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
741774 await renameChat ( dashboardAgentDb , {
742775 chatId,
743776 userId,
777+ actingUserId,
744778 organizationId : project . organizationId ,
745779 title : parsed . data . title ,
746780 } ) ;
@@ -751,6 +785,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
751785 await setChatPinned ( dashboardAgentDb , {
752786 chatId,
753787 userId,
788+ actingUserId,
754789 organizationId : project . organizationId ,
755790 pinned : parsed . data . pinned === "true" ,
756791 } ) ;
@@ -762,6 +797,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
762797 await markChatRead ( dashboardAgentDb , {
763798 chatId,
764799 userId,
800+ actingUserId,
765801 organizationId : project . organizationId ,
766802 } ) ;
767803 return json ( { ok : true } ) ;
@@ -774,6 +810,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
774810 ! ( await chatExists ( dashboardAgentDb , {
775811 chatId,
776812 userId,
813+ actingUserId,
777814 organizationId : project . organizationId ,
778815 } ) )
779816 ) {
@@ -783,6 +820,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
783820 const { cancelledWatches } = await deleteChatWithWatches ( {
784821 chatId,
785822 userId,
823+ actingUserId,
786824 organizationId : project . organizationId ,
787825 } ) ;
788826 return json ( { ok : true , cancelledWatches } ) ;
@@ -803,6 +841,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
803841 ! ( await chatExists ( dashboardAgentDb , {
804842 chatId,
805843 userId,
844+ actingUserId,
806845 organizationId : project . organizationId ,
807846 } ) )
808847 ) {
0 commit comments