Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit abb653a

Browse files
Merging cf9a51d into trunk-temp/pr-3959/5c161013-fb76-4633-a9db-b4094fd3c913
2 parents e59be41 + cf9a51d commit abb653a

6 files changed

Lines changed: 115 additions & 14 deletions

File tree

packages/core/src/sessions/cloudTaskUpdateNotifications.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function snapshotUpdate(
9393
};
9494
}
9595

96-
function createHarness() {
96+
function createHarness(isTaskAuthor = true) {
9797
const sessions: Record<string, AgentSession> = {};
9898
const store = {
9999
getSessions: () => sessions,
@@ -197,6 +197,8 @@ function createHarness() {
197197
const service = new SessionService(deps);
198198
service.watchCloudTask(TASK_ID, RUN_ID, "https://us.posthog.com", 1);
199199
if (!onUpdate) throw new Error("watchCloudTask did not subscribe");
200+
const session = sessions[RUN_ID];
201+
if (session) session.isTaskAuthor = isTaskAuthor;
200202

201203
return {
202204
sendUpdate: (update: CloudTaskUpdatePayload) => onUpdate?.(update),
@@ -208,6 +210,25 @@ function createHarness() {
208210
}
209211

210212
describe("cloud task update notifications", () => {
213+
it("does not notify a non-owner watching the task", () => {
214+
const harness = createHarness(false);
215+
216+
harness.sendUpdate(
217+
logsUpdate(
218+
[
219+
sessionPrompt(1),
220+
permissionRequest("request-1", "tool-1"),
221+
turnComplete(),
222+
],
223+
3,
224+
),
225+
);
226+
227+
expect(harness.notifyPromptComplete).not.toHaveBeenCalled();
228+
expect(harness.notifyPermissionRequest).not.toHaveBeenCalled();
229+
expect(harness.enqueueSpeech).not.toHaveBeenCalled();
230+
});
231+
211232
it("does not notify for turn_completes replayed in a snapshot", () => {
212233
const harness = createHarness();
213234

packages/core/src/sessions/sessionFactory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export function createBaseSession(
99
taskRunId,
1010
taskId,
1111
taskTitle,
12+
isTaskAuthor: true,
1213
channel: `agent-event:${taskRunId}`,
1314
events: [],
1415
startedAt: Date.now(),

packages/core/src/sessions/sessionService.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ export interface ReconcileTaskConnectionParams {
432432
session: ReconcileSessionState | undefined;
433433
repoPath: string | null;
434434
isCloud: boolean;
435+
isTaskAuthor?: boolean;
435436
isSuspended?: boolean;
436437
isOnline: boolean;
437438
cloudAuth: CloudConnectionAuth;
@@ -2967,13 +2968,15 @@ export class SessionService {
29672968
stopReason === "end_turn" &&
29682969
session.messageQueue.length === 0
29692970
) {
2970-
this.d.notifyPromptComplete(
2971-
session.taskTitle,
2972-
stopReason,
2973-
session.taskId,
2974-
turnStartedAtTs ? acpMsg.ts - turnStartedAtTs : undefined,
2975-
);
2976-
this.speakDeterministic(taskRunId, session, "done");
2971+
if (session.isTaskAuthor !== false) {
2972+
this.d.notifyPromptComplete(
2973+
session.taskTitle,
2974+
stopReason,
2975+
session.taskId,
2976+
turnStartedAtTs ? acpMsg.ts - turnStartedAtTs : undefined,
2977+
);
2978+
this.speakDeterministic(taskRunId, session, "done");
2979+
}
29772980
}
29782981
this.d.taskViewedApi.markActivity(session.taskId);
29792982
this.finalizeTurnContent(taskRunId, "turn_complete", acpMsg.ts);
@@ -3137,7 +3140,11 @@ export class SessionService {
31373140
: this.drainQueuedMessages(taskRunId, session);
31383141

31393142
// Only notify when nothing is sendable - queued messages start a new turn
3140-
if (stopReason && !hasSendableMessages) {
3143+
if (
3144+
stopReason &&
3145+
!hasSendableMessages &&
3146+
session.isTaskAuthor !== false
3147+
) {
31413148
this.d.notifyPromptComplete(
31423149
session.taskTitle,
31433150
stopReason,
@@ -3383,8 +3390,10 @@ export class SessionService {
33833390

33843391
this.d.store.setPendingPermissions(taskRunId, newPermissions);
33853392
this.d.taskViewedApi.markActivity(session.taskId);
3386-
this.d.notifyPermissionRequest(session.taskTitle, session.taskId);
3387-
this.speakDeterministic(taskRunId, session, "needs_input");
3393+
if (session.isTaskAuthor !== false) {
3394+
this.d.notifyPermissionRequest(session.taskTitle, session.taskId);
3395+
this.speakDeterministic(taskRunId, session, "needs_input");
3396+
}
33883397
}
33893398

33903399
private handleCloudPermissionRequest(
@@ -3441,8 +3450,10 @@ export class SessionService {
34413450

34423451
this.d.store.setPendingPermissions(taskRunId, newPermissions);
34433452
this.d.taskViewedApi.markActivity(session.taskId);
3444-
this.d.notifyPermissionRequest(session.taskTitle, session.taskId);
3445-
this.speakDeterministic(taskRunId, session, "needs_input");
3453+
if (session.isTaskAuthor !== false) {
3454+
this.d.notifyPermissionRequest(session.taskTitle, session.taskId);
3455+
this.speakDeterministic(taskRunId, session, "needs_input");
3456+
}
34463457
}
34473458

34483459
private surfacePersistedPendingPermissions(
@@ -5611,6 +5622,7 @@ export class SessionService {
56115622
runStatus?: TaskRunStatus,
56125623
initialReasoningEffort?: string,
56135624
runState?: Record<string, unknown>,
5625+
isTaskAuthor = true,
56145626
): () => void {
56155627
const taskRunId = runId;
56165628
const persistedConfigOptions = this.d.getPersistedConfigOptions(taskRunId);
@@ -5675,6 +5687,9 @@ export class SessionService {
56755687
// Ensure configOptions is populated on revisit
56765688
const existing = this.d.store.getSessionByTaskId(taskId);
56775689
if (existing) {
5690+
if (existing.isTaskAuthor !== isTaskAuthor) {
5691+
this.d.store.updateSession(existing.taskRunId, { isTaskAuthor });
5692+
}
56785693
const existingMode = getConfigOptionByCategory(
56795694
existing.configOptions,
56805695
"mode",
@@ -5801,6 +5816,7 @@ export class SessionService {
58015816
const session = createBaseSession(taskRunId, taskId, taskTitle);
58025817
session.status = "disconnected";
58035818
session.isCloud = true;
5819+
session.isTaskAuthor = isTaskAuthor;
58045820
session.adapter = adapter;
58055821
session.configOptions = buildInitialConfigOptions(
58065822
initialMode,
@@ -5814,6 +5830,9 @@ export class SessionService {
58145830
} else {
58155831
// Ensure cloud flag and configOptions are set on existing sessions
58165832
const updates: Partial<AgentSession> = {};
5833+
if (existing.isTaskAuthor !== isTaskAuthor) {
5834+
updates.isTaskAuthor = isTaskAuthor;
5835+
}
58175836
if (!existing.isCloud) updates.isCloud = true;
58185837
if (existing.adapter !== adapter) updates.adapter = adapter;
58195838
if (!existing.configOptions?.length || existing.adapter !== adapter) {
@@ -6851,6 +6870,7 @@ export class SessionService {
68516870
session,
68526871
repoPath,
68536872
isCloud,
6873+
isTaskAuthor = true,
68546874
isSuspended,
68556875
isOnline,
68566876
cloudAuth,
@@ -6866,6 +6886,7 @@ export class SessionService {
68666886
task,
68676887
cloudAuth,
68686888
onCloudStatusChange,
6889+
isTaskAuthor,
68696890
);
68706891
}
68716892

@@ -6952,6 +6973,7 @@ export class SessionService {
69526973
task: Task,
69536974
cloudAuth: CloudConnectionAuth,
69546975
onCloudStatusChange?: () => void,
6976+
isTaskAuthor = true,
69556977
): () => void {
69566978
this.updateSessionTaskTitle(
69576979
task.id,
@@ -6989,6 +7011,7 @@ export class SessionService {
69897011
task.latest_run?.status,
69907012
initialReasoningEffort,
69917013
task.latest_run?.state,
7014+
isTaskAuthor,
69927015
);
69937016
}
69947017

packages/shared/src/sessions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface AgentSession {
5050
taskRunId: string;
5151
taskId: string;
5252
taskTitle: string;
53+
isTaskAuthor?: boolean;
5354
channel: string;
5455
events: AcpMessage[];
5556
startedAt: number;

packages/ui/src/features/sessions/hooks/useSessionConnection.test.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ const mocks = vi.hoisted(() => {
99
startActivityHeartbeat: vi.fn(() => () => {}),
1010
reconcileTaskConnection: vi.fn(() => () => {}),
1111
};
12-
return { sessionService, unregisterMountedTask };
12+
return {
13+
currentUser: { uuid: "user-1" } as { uuid: string } | undefined,
14+
sessionService,
15+
unregisterMountedTask,
16+
};
1317
});
1418

1519
vi.mock("@posthog/di/react", () => ({
@@ -34,6 +38,14 @@ vi.mock("@posthog/ui/features/auth/store", () => ({
3438
}),
3539
}));
3640

41+
vi.mock("@posthog/ui/features/auth/authClient", () => ({
42+
useOptionalAuthenticatedClient: () => ({}),
43+
}));
44+
45+
vi.mock("@posthog/ui/features/auth/useCurrentUser", () => ({
46+
useCurrentUser: () => ({ data: mocks.currentUser }),
47+
}));
48+
3749
vi.mock("./useChatTitleGenerator", () => ({
3850
useChatTitleGenerator: vi.fn(),
3951
}));
@@ -56,7 +68,9 @@ function connectionProps(taskId: string) {
5668

5769
describe("useSessionConnection mounted-task registration", () => {
5870
beforeEach(() => {
71+
mocks.currentUser = { uuid: "user-1" };
5972
mocks.sessionService.registerMountedTask.mockClear();
73+
mocks.sessionService.reconcileTaskConnection.mockClear();
6074
mocks.unregisterMountedTask.mockClear();
6175
});
6276

@@ -91,4 +105,36 @@ describe("useSessionConnection mounted-task registration", () => {
91105
unmount();
92106
expect(mocks.unregisterMountedTask).toHaveBeenCalledTimes(2);
93107
});
108+
109+
it.each([
110+
[
111+
"the current user is still loading",
112+
undefined,
113+
{ uuid: "author" },
114+
undefined,
115+
],
116+
["the task has no author", { uuid: "user-1" }, undefined, undefined],
117+
[
118+
"the current user is the author",
119+
{ uuid: "user-1" },
120+
{ uuid: "user-1" },
121+
true,
122+
],
123+
[
124+
"another user authored the task",
125+
{ uuid: "user-1" },
126+
{ uuid: "user-2" },
127+
false,
128+
],
129+
])("sets authorship when %s", (_case, currentUser, createdBy, expected) => {
130+
mocks.currentUser = currentUser;
131+
const props = connectionProps("task-1");
132+
props.task.created_by = createdBy as Task["created_by"];
133+
134+
renderHook(() => useSessionConnection(props));
135+
136+
expect(mocks.sessionService.reconcileTaskConnection).toHaveBeenCalledWith(
137+
expect.objectContaining({ isTaskAuthor: expected }),
138+
);
139+
});
94140
});

packages/ui/src/features/sessions/hooks/useSessionConnection.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
} from "@posthog/core/sessions/sessionService";
66
import { useService } from "@posthog/di/react";
77
import type { Task } from "@posthog/shared/domain-types";
8+
import { useOptionalAuthenticatedClient } from "@posthog/ui/features/auth/authClient";
89
import { useAuthStateValue } from "@posthog/ui/features/auth/store";
10+
import { useCurrentUser } from "@posthog/ui/features/auth/useCurrentUser";
911
import type { AgentSession } from "@posthog/ui/features/sessions/sessionStore";
1012
import { useConnectivity } from "@posthog/ui/hooks/useConnectivity";
1113
import { useUserPresence } from "@posthog/ui/hooks/useUserPresence";
@@ -33,6 +35,8 @@ export function useSessionConnection({
3335
isSuspended,
3436
}: UseSessionConnectionOptions) {
3537
const queryClient = useQueryClient();
38+
const client = useOptionalAuthenticatedClient();
39+
const { data: currentUser } = useCurrentUser({ client });
3640
const { isOnline } = useConnectivity();
3741
const cloudAuthState = useAuthStateValue((state) => state);
3842
const sessionService = useService<SessionService>(SESSION_SERVICE);
@@ -99,6 +103,10 @@ export function useSessionConnection({
99103
session: connectionSession,
100104
repoPath,
101105
isCloud,
106+
isTaskAuthor:
107+
currentUser?.uuid && task.created_by?.uuid
108+
? currentUser.uuid === task.created_by.uuid
109+
: undefined,
102110
// While the user is away, freeze local reconciling too — otherwise the
103111
// idle-kill auto-reconnect would respawn the agent process seconds
104112
// after the server reclaimed it. Cloud reconcile ignores this flag.
@@ -119,6 +127,7 @@ export function useSessionConnection({
119127
connectionSession,
120128
repoPath,
121129
isCloud,
130+
currentUser?.uuid,
122131
isSuspended,
123132
userPresent,
124133
isOnline,

0 commit comments

Comments
 (0)