Skip to content

Commit d583d63

Browse files
committed
scope builder sessions to the auth identity
1 parent b89e9c6 commit d583d63

6 files changed

Lines changed: 72 additions & 20 deletions

File tree

packages/ui/src/features/loops/components/LoopsListView.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,13 @@ export const WithBuilderSessions: Story = {
181181
taskId: "builder-task-1",
182182
prompt: "Summarize my open PRs every weekday morning",
183183
startedAt: 1752000000000,
184+
identity: "us:2",
184185
},
185186
{
186187
taskId: "builder-task-2",
187188
prompt: "Build a loop",
188189
startedAt: 1752000600000,
190+
identity: "us:2",
189191
},
190192
],
191193
onResumeBuilderSession: () => {},

packages/ui/src/features/loops/hooks/useLoopBuilderSessions.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { useArchivedTaskIds } from "@posthog/ui/features/archive/useArchivedTaskIds";
2+
import {
3+
getAuthIdentity,
4+
useAuthStateValue,
5+
} from "@posthog/ui/features/auth/store";
26
import { useTaskSummaries } from "@posthog/ui/features/tasks/useTasks";
37
import { useEffect, useMemo, useState } from "react";
48
import {
@@ -12,14 +16,24 @@ import {
1216
} from "../loopBuilderSessionStore";
1317

1418
/**
15-
* The recorded builder sessions whose cloud run is still alive. Sessions whose
16-
* sandbox has shut down (run completed, failed, cancelled, or task archived or
17-
* deleted) are pruned from the persisted store as their status comes in, so the
18-
* "in progress" list never offers a resume into a dead session. The liveness
19-
* decision itself is the pure `isBuilderSessionEnded`.
19+
* The current identity's builder sessions whose cloud run is still alive.
20+
* Sessions whose sandbox has shut down (run completed, failed, cancelled, or
21+
* task archived or deleted) are pruned from the persisted store as their status
22+
* comes in, so the "in progress" list never offers a resume into a dead
23+
* session. Other identities' sessions are never shown or pruned: the summaries
24+
* this hook queries are only authoritative for the signed-in account. The
25+
* liveness decision itself is the pure `isBuilderSessionEnded`.
2026
*/
2127
export function useLoopBuilderSessions(): LoopBuilderSession[] {
22-
const sessions = useLoopBuilderSessionStore((state) => state.sessions);
28+
const identity = useAuthStateValue(getAuthIdentity);
29+
const allSessions = useLoopBuilderSessionStore((state) => state.sessions);
30+
const sessions = useMemo(
31+
() =>
32+
identity
33+
? allSessions.filter((session) => session.identity === identity)
34+
: [],
35+
[allSessions, identity],
36+
);
2337
const archivedTaskIds = useArchivedTaskIds();
2438
const taskIds = useMemo(
2539
() => sessions.map((session) => session.taskId),
@@ -58,14 +72,15 @@ export function useLoopBuilderSessions(): LoopBuilderSession[] {
5872
}, [sessions, now]);
5973

6074
useEffect(() => {
61-
if (!summaries) return;
75+
if (!summaries || !identity) return;
6276
const store = useLoopBuilderSessionStore.getState();
6377
for (const session of store.sessions) {
78+
if (session.identity !== identity) continue;
6479
if (isBuilderSessionEnded(session, summaries, archivedTaskIds, now)) {
6580
store.removeSession(session.taskId);
6681
}
6782
}
68-
}, [summaries, archivedTaskIds, now]);
83+
}, [summaries, archivedTaskIds, now, identity]);
6984

7085
return useMemo(() => {
7186
if (!summaries) {

packages/ui/src/features/loops/hooks/useLoopBuilderTask.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { TaskCreationInput } from "@posthog/core/task-detail/taskService";
22
import type { Task } from "@posthog/shared/domain-types";
3+
import { getAuthIdentity, useAuthStore } from "@posthog/ui/features/auth/store";
34
import {
45
type InboxCloudTaskInputContext,
56
useInboxCloudTaskRunner,
@@ -77,10 +78,13 @@ export function useLoopBuilderTask(context?: {
7778
);
7879

7980
const handleTaskCreated = useCallback((task: Task) => {
81+
const identity = getAuthIdentity(useAuthStore.getState().authState);
82+
if (!identity) return;
8083
useLoopBuilderSessionStore.getState().addSession({
8184
taskId: task.id,
8285
prompt: instructionsRef.current.trim() || "Build a loop",
8386
startedAt: Date.now(),
87+
identity,
8488
});
8589
}, []);
8690

packages/ui/src/features/loops/loopBuilderLiveness.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import type { LoopBuilderSession } from "./loopBuilderSessionStore";
99
const NOW = 1_752_000_000_000;
1010

1111
function session(ageMs: number): LoopBuilderSession {
12-
return { taskId: "task-1", prompt: "prompt", startedAt: NOW - ageMs };
12+
return {
13+
taskId: "task-1",
14+
prompt: "prompt",
15+
startedAt: NOW - ageMs,
16+
identity: "us:1",
17+
};
1318
}
1419

1520
function summaries(

packages/ui/src/features/loops/loopBuilderSessionStore.test.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ vi.mock("@posthog/ui/shell/rendererStorage", () => ({
1414
flushRendererStateWrites: async () => {},
1515
}));
1616

17-
function session(taskId: string, startedAt = 0): LoopBuilderSession {
18-
return { taskId, prompt: `prompt ${taskId}`, startedAt };
17+
function session(
18+
taskId: string,
19+
startedAt = 0,
20+
identity = "us:1",
21+
): LoopBuilderSession {
22+
return { taskId, prompt: `prompt ${taskId}`, startedAt, identity };
1923
}
2024

2125
beforeEach(() => {
@@ -35,20 +39,28 @@ it("replaces an existing session with the same task id", () => {
3539
const store = useLoopBuilderSessionStore.getState();
3640
store.addSession(session("a", 1));
3741
store.addSession(session("b", 2));
38-
store.addSession({ taskId: "a", prompt: "updated", startedAt: 3 });
42+
store.addSession({
43+
taskId: "a",
44+
prompt: "updated",
45+
startedAt: 3,
46+
identity: "us:1",
47+
});
3948
const sessions = useLoopBuilderSessionStore.getState().sessions;
4049
expect(sessions.map((s) => s.taskId)).toEqual(["a", "b"]);
4150
expect(sessions[0]?.prompt).toBe("updated");
4251
});
4352

44-
it("caps the list at the max session count", () => {
53+
it("caps sessions per identity without evicting other identities", () => {
4554
const store = useLoopBuilderSessionStore.getState();
55+
store.addSession(session("other", 0, "eu:2"));
4656
for (let i = 0; i < MAX_BUILDER_SESSIONS + 2; i++) {
4757
store.addSession(session(`task-${i}`, i));
4858
}
4959
const sessions = useLoopBuilderSessionStore.getState().sessions;
50-
expect(sessions).toHaveLength(MAX_BUILDER_SESSIONS);
51-
expect(sessions[0]?.taskId).toBe(`task-${MAX_BUILDER_SESSIONS + 1}`);
60+
const mine = sessions.filter((s) => s.identity === "us:1");
61+
expect(mine).toHaveLength(MAX_BUILDER_SESSIONS);
62+
expect(mine[0]?.taskId).toBe(`task-${MAX_BUILDER_SESSIONS + 1}`);
63+
expect(sessions.some((s) => s.taskId === "other")).toBe(true);
5264
});
5365

5466
it.each([

packages/ui/src/features/loops/loopBuilderSessionStore.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export interface LoopBuilderSession {
99
taskId: string;
1010
prompt: string;
1111
startedAt: number;
12+
/** Auth identity (`getAuthIdentity`) the session belongs to. Sessions are
13+
* only shown, pruned and capped within their own identity, so prompts never
14+
* leak across accounts or projects sharing a device. */
15+
identity: string;
1216
}
1317

1418
export const MAX_BUILDER_SESSIONS = 5;
@@ -26,12 +30,19 @@ export const useLoopBuilderSessionStore = create<LoopBuilderSessionState>()(
2630
// Flushed immediately: adding is followed by navigating away, and a lost
2731
// debounced write is exactly the "can't find my builder" bug again.
2832
addSession: (session) => {
29-
set((state) => ({
30-
sessions: [
33+
set((state) => {
34+
const others = state.sessions.filter(
35+
(s) => s.identity !== session.identity,
36+
);
37+
const mine = [
3138
session,
32-
...state.sessions.filter((s) => s.taskId !== session.taskId),
33-
].slice(0, MAX_BUILDER_SESSIONS),
34-
}));
39+
...state.sessions.filter(
40+
(s) =>
41+
s.identity === session.identity && s.taskId !== session.taskId,
42+
),
43+
].slice(0, MAX_BUILDER_SESSIONS);
44+
return { sessions: [...mine, ...others] };
45+
});
3546
void flushRendererStateWrites();
3647
},
3748
removeSession: (taskId) => {
@@ -45,6 +56,9 @@ export const useLoopBuilderSessionStore = create<LoopBuilderSessionState>()(
4556
name: "posthog-code-loop-builder-sessions",
4657
storage: electronStorage,
4758
partialize: (state) => ({ sessions: state.sessions }),
59+
// v0 entries had no identity and can't be attributed; drop them.
60+
version: 1,
61+
migrate: () => ({ sessions: [] as LoopBuilderSession[] }),
4862
},
4963
),
5064
);

0 commit comments

Comments
 (0)