Skip to content

Commit 559c741

Browse files
authored
fix(cloud): keep latest warm image lease
Ignore stale warm responses after the sandbox selection changes so an older image cannot replace the lease for the latest environment or custom image. Generated-By: PostHog Code Task-Id: 6b752fb1-4fa1-45e4-920a-38306e4b7cee
1 parent 496e683 commit 559c741

2 files changed

Lines changed: 69 additions & 2 deletions

File tree

packages/ui/src/features/task-detail/hooks/useWarmTask.test.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ vi.mock("../../../shell/logger", () => ({
1818
}));
1919

2020
import { useWarmTask } from "./useWarmTask";
21+
import { takeWarmTaskLease } from "./warmTaskLease";
2122

2223
interface Props {
2324
workspaceMode: WorkspaceMode;
@@ -238,6 +239,68 @@ describe("useWarmTask", () => {
238239
expect(mockClient.warmTask).toHaveBeenCalledTimes(2);
239240
});
240241

242+
it("warms only the latest image when selection changes during the debounce", async () => {
243+
const { rerender } = renderHook((props: Props) => useWarmTask(props), {
244+
initialProps: { ...cloudTyping, customImageId: "image-123" },
245+
});
246+
247+
rerender({ ...cloudTyping, customImageId: "image-456" });
248+
await flushDebounce();
249+
250+
expect(mockClient.warmTask).toHaveBeenCalledOnce();
251+
expect(mockClient.warmTask).toHaveBeenCalledWith({
252+
repository: "acme/repo",
253+
github_integration: 42,
254+
branch: "main",
255+
...NULL_RUNTIME,
256+
custom_image_id: "image-456",
257+
});
258+
});
259+
260+
it("keeps the latest image lease when warm responses complete out of order", async () => {
261+
type WarmResponse = { task_id: string; run_id: string };
262+
let resolveFirstWarm!: (value: WarmResponse) => void;
263+
let resolveSecondWarm!: (value: WarmResponse) => void;
264+
const firstWarm = new Promise<WarmResponse>((resolve) => {
265+
resolveFirstWarm = resolve;
266+
});
267+
const secondWarm = new Promise<WarmResponse>((resolve) => {
268+
resolveSecondWarm = resolve;
269+
});
270+
mockClient.warmTask
271+
.mockReturnValueOnce(firstWarm)
272+
.mockReturnValueOnce(secondWarm);
273+
274+
const { rerender } = renderHook((props: Props) => useWarmTask(props), {
275+
initialProps: { ...cloudTyping, customImageId: "image-123" },
276+
});
277+
await flushDebounce();
278+
279+
rerender({ ...cloudTyping, customImageId: "image-456" });
280+
await flushDebounce();
281+
282+
await act(async () => {
283+
resolveSecondWarm({ task_id: "task-2", run_id: "run-2" });
284+
await secondWarm;
285+
});
286+
await act(async () => {
287+
resolveFirstWarm({ task_id: "task-1", run_id: "run-1" });
288+
await firstWarm;
289+
});
290+
291+
expect(
292+
takeWarmTaskLease({
293+
repository: "acme/repo",
294+
branch: "main",
295+
runtimeAdapter: null,
296+
model: null,
297+
reasoningEffort: null,
298+
sandboxEnvironmentId: null,
299+
customImageId: "image-456",
300+
}),
301+
).toEqual({ taskId: "task-2", runId: "run-2" });
302+
});
303+
241304
it("warms again for a new selection after a failed warm", async () => {
242305
mockClient.warmTask.mockRejectedValueOnce(new Error("boom"));
243306
const { rerender } = renderHook((props: Props) => useWarmTask(props), {

packages/ui/src/features/task-detail/hooks/useWarmTask.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function useWarmTask({
4242

4343
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
4444
const lastWarmedKeyRef = useRef<string | null>(null);
45+
const latestKeyRef = useRef<string | null>(null);
4546

4647
const isCloud = workspaceMode === "cloud";
4748
const normalizedBranch = branch ?? null;
@@ -69,6 +70,7 @@ export function useWarmTask({
6970
customImageId: normalizedCustomImageId,
7071
})}`
7172
: null;
73+
latestKeyRef.current = key;
7274

7375
useEffect(() => {
7476
const clearDebounce = (): void => {
@@ -111,7 +113,7 @@ export function useWarmTask({
111113
...(warmCustomImageId ? { custom_image_id: warmCustomImageId } : {}),
112114
})
113115
.then((warm) => {
114-
if (warm) {
116+
if (warm && latestKeyRef.current === key) {
115117
rememberWarmTaskLease(
116118
buildWarmTaskLeaseKey({
117119
repository,
@@ -127,7 +129,9 @@ export function useWarmTask({
127129
}
128130
})
129131
.catch((error) => {
130-
lastWarmedKeyRef.current = null;
132+
if (latestKeyRef.current === key) {
133+
lastWarmedKeyRef.current = null;
134+
}
131135
log.warn("Failed to warm task", { error });
132136
});
133137
}, WARM_DEBOUNCE_MS);

0 commit comments

Comments
 (0)