Skip to content

Commit 3a9c76c

Browse files
authored
fix(mobile): prewarm the selected sandbox image (port #3511)
Thread the selected sandbox environment / custom base image through the mobile cloud-task warm path so a warmed sandbox matches what the task will actually run on. - `warmTask()` now optionally includes `sandbox_environment_id` / `custom_image_id` in the request body (only when set). - `useWarmTask` folds both ids into the debounce/dedupe key, so changing the selection re-warms, and forwards them to the warm call. - `runTaskInCloud` carries the ids on the run so a reused warm sandbox matches the selection instead of a mismatched default. Generated-By: PostHog Code Task-Id: 5c715645-baa3-460e-91ca-73ee7e7fad86
1 parent e11448d commit 3a9c76c

5 files changed

Lines changed: 149 additions & 1 deletion

File tree

apps/mobile/src/features/tasks/api.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,30 @@ describe("runTaskInCloud", () => {
6363
expect(init.body).toBeUndefined();
6464
});
6565

66+
it("forwards the selected sandbox environment and custom image", async () => {
67+
await runTaskInCloud("task-1", {
68+
sandboxEnvironmentId: "environment-123",
69+
customImageId: "image-123",
70+
});
71+
72+
expect(bodyOf(mockFetch.mock.calls[0])).toMatchObject({
73+
sandbox_environment_id: "environment-123",
74+
custom_image_id: "image-123",
75+
});
76+
});
77+
78+
it("omits the sandbox environment and custom image when unset", async () => {
79+
await runTaskInCloud("task-1", {
80+
model: "claude-opus-4-8",
81+
sandboxEnvironmentId: null,
82+
customImageId: null,
83+
});
84+
85+
const body = bodyOf(mockFetch.mock.calls[0]);
86+
expect(body).not.toHaveProperty("sandbox_environment_id");
87+
expect(body).not.toHaveProperty("custom_image_id");
88+
});
89+
6690
it("sends rtk_enabled=false when the run opts out", async () => {
6791
await runTaskInCloud("task-1", { rtkEnabled: false });
6892

apps/mobile/src/features/tasks/api.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@ export async function warmTask(options: {
314314
runtime_adapter?: string | null;
315315
model?: string | null;
316316
reasoning_effort?: string | null;
317+
sandbox_environment_id?: string | null;
318+
custom_image_id?: string | null;
317319
}): Promise<{ task_id: string; run_id: string } | null> {
318320
const baseUrl = getBaseUrl();
319321
const projectId = getProjectId();
@@ -329,6 +331,12 @@ export async function warmTask(options: {
329331
runtime_adapter: options.runtime_adapter ?? null,
330332
model: options.model ?? null,
331333
reasoning_effort: options.reasoning_effort ?? null,
334+
...(options.sandbox_environment_id
335+
? { sandbox_environment_id: options.sandbox_environment_id }
336+
: {}),
337+
...(options.custom_image_id
338+
? { custom_image_id: options.custom_image_id }
339+
: {}),
332340
}),
333341
},
334342
);
@@ -431,6 +439,10 @@ export interface RunTaskInCloudOptions {
431439
model?: string;
432440
/** Reasoning effort: "low" | "medium" | "high" (model-dependent). */
433441
reasoningEffort?: string;
442+
/** Sandbox environment / custom base image to run on. Sent so a reused warm
443+
* sandbox matches the selection instead of a mismatched default. */
444+
sandboxEnvironmentId?: string | null;
445+
customImageId?: string | null;
434446
/** Permission mode: "default" | "acceptEdits" | "plan" | "auto". */
435447
initialPermissionMode?: string;
436448
/** Source that triggered this run. */
@@ -463,6 +475,8 @@ export async function runTaskInCloud(
463475
options.runtimeAdapter !== undefined ||
464476
options.model !== undefined ||
465477
options.reasoningEffort !== undefined ||
478+
options.sandboxEnvironmentId !== undefined ||
479+
options.customImageId !== undefined ||
466480
options.initialPermissionMode !== undefined ||
467481
options.runSource !== undefined ||
468482
options.signalReportId !== undefined ||
@@ -488,6 +502,12 @@ export async function runTaskInCloud(
488502
payload.reasoning_effort = options.reasoningEffort;
489503
}
490504
}
505+
if (options?.sandboxEnvironmentId) {
506+
payload.sandbox_environment_id = options.sandboxEnvironmentId;
507+
}
508+
if (options?.customImageId) {
509+
payload.custom_image_id = options.customImageId;
510+
}
491511
if (options?.initialPermissionMode) {
492512
payload.initial_permission_mode = options.initialPermissionMode;
493513
}

apps/mobile/src/features/tasks/api.warm.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,58 @@ describe("warmTask", () => {
9090
);
9191
});
9292

93+
it("forwards the selected sandbox environment and custom image", async () => {
94+
mockFetch.mockResolvedValueOnce(
95+
new Response(JSON.stringify({ task_id: "task-1", run_id: "run-1" }), {
96+
status: 200,
97+
}),
98+
);
99+
100+
await warmTask({
101+
repository: "posthog/posthog",
102+
github_integration: 7,
103+
branch: "main",
104+
sandbox_environment_id: "environment-123",
105+
custom_image_id: "image-123",
106+
});
107+
108+
expect(mockFetch).toHaveBeenCalledWith(
109+
"https://app.posthog.test/api/projects/42/tasks/warm/",
110+
expect.objectContaining({
111+
body: JSON.stringify({
112+
repository: "posthog/posthog",
113+
github_integration: 7,
114+
branch: "main",
115+
runtime_adapter: null,
116+
model: null,
117+
reasoning_effort: null,
118+
sandbox_environment_id: "environment-123",
119+
custom_image_id: "image-123",
120+
}),
121+
}),
122+
);
123+
});
124+
125+
it("omits the sandbox environment and custom image when unset", async () => {
126+
mockFetch.mockResolvedValueOnce(
127+
new Response(JSON.stringify({ task_id: "task-1", run_id: "run-1" }), {
128+
status: 200,
129+
}),
130+
);
131+
132+
await warmTask({
133+
repository: "posthog/posthog",
134+
github_integration: 7,
135+
sandbox_environment_id: null,
136+
custom_image_id: null,
137+
});
138+
139+
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit];
140+
const body = JSON.parse(init.body as string);
141+
expect(body).not.toHaveProperty("sandbox_environment_id");
142+
expect(body).not.toHaveProperty("custom_image_id");
143+
});
144+
93145
it("serializes a missing branch as null", async () => {
94146
mockFetch.mockResolvedValueOnce(
95147
new Response(JSON.stringify({ task_id: "task-1", run_id: "run-1" }), {

apps/mobile/src/features/tasks/hooks/useWarmTask.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ interface Props {
3232
runtimeAdapter?: string | null;
3333
model?: string | null;
3434
reasoningEffort?: string | null;
35+
sandboxEnvironmentId?: string | null;
36+
customImageId?: string | null;
3537
}
3638

3739
const composing: Props = {
@@ -198,6 +200,42 @@ describe("useWarmTask", () => {
198200
},
199201
);
200202

203+
it("forwards the sandbox environment and custom image", async () => {
204+
render({
205+
...composing,
206+
sandboxEnvironmentId: "environment-123",
207+
customImageId: "image-123",
208+
});
209+
await flushDebounce();
210+
211+
expect(mockWarmTask).toHaveBeenCalledWith({
212+
repository: "acme/repo",
213+
github_integration: 42,
214+
branch: "main",
215+
...NULL_RUNTIME,
216+
sandbox_environment_id: "environment-123",
217+
custom_image_id: "image-123",
218+
});
219+
});
220+
221+
it("re-warms when the custom image changes", async () => {
222+
const { rerender } = render({ ...composing, customImageId: "image-123" });
223+
await flushDebounce();
224+
expect(mockWarmTask).toHaveBeenCalledOnce();
225+
226+
rerender({ ...composing, customImageId: "image-456" });
227+
await flushDebounce();
228+
229+
expect(mockWarmTask).toHaveBeenCalledTimes(2);
230+
expect(mockWarmTask).toHaveBeenLastCalledWith({
231+
repository: "acme/repo",
232+
github_integration: 42,
233+
branch: "main",
234+
...NULL_RUNTIME,
235+
custom_image_id: "image-456",
236+
});
237+
});
238+
201239
it("warms again for a new selection after a failed warm", async () => {
202240
mockWarmTask.mockRejectedValueOnce(new Error("boom"));
203241
const { rerender } = render(composing);

apps/mobile/src/features/tasks/hooks/useWarmTask.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ interface UseWarmTaskOptions {
1616
runtimeAdapter?: string | null;
1717
model?: string | null;
1818
reasoningEffort?: string | null;
19+
sandboxEnvironmentId?: string | null;
20+
customImageId?: string | null;
1921
}
2022

2123
export function useWarmTask({
@@ -26,6 +28,8 @@ export function useWarmTask({
2628
runtimeAdapter,
2729
model,
2830
reasoningEffort,
31+
sandboxEnvironmentId,
32+
customImageId,
2933
}: UseWarmTaskOptions): void {
3034
const enabled = useFeatureFlag(TASKS_PREWARM_SANDBOX_FLAG);
3135

@@ -36,14 +40,16 @@ export function useWarmTask({
3640
const normalizedRuntimeAdapter = runtimeAdapter ?? null;
3741
const normalizedModel = model ?? null;
3842
const normalizedReasoningEffort = reasoningEffort ?? null;
43+
const normalizedSandboxEnvironmentId = sandboxEnvironmentId ?? null;
44+
const normalizedCustomImageId = customImageId ?? null;
3945
const eligible =
4046
!!enabled &&
4147
!!repository &&
4248
githubIntegrationId != null &&
4349
!composerIsEmpty;
4450
const key =
4551
repository && githubIntegrationId != null
46-
? `${githubIntegrationId}:${repository}:${normalizedBranch ?? ""}:${normalizedRuntimeAdapter ?? ""}:${normalizedModel ?? ""}:${normalizedReasoningEffort ?? ""}`
52+
? `${githubIntegrationId}:${repository}:${normalizedBranch ?? ""}:${normalizedRuntimeAdapter ?? ""}:${normalizedModel ?? ""}:${normalizedReasoningEffort ?? ""}:${normalizedSandboxEnvironmentId ?? ""}:${normalizedCustomImageId ?? ""}`
4753
: null;
4854

4955
useEffect(() => {
@@ -68,6 +74,8 @@ export function useWarmTask({
6874
const warmRuntimeAdapter = normalizedRuntimeAdapter;
6975
const warmModel = normalizedModel;
7076
const warmReasoningEffort = normalizedReasoningEffort;
77+
const warmSandboxEnvironmentId = normalizedSandboxEnvironmentId;
78+
const warmCustomImageId = normalizedCustomImageId;
7179
debounceRef.current = setTimeout(() => {
7280
debounceRef.current = null;
7381
lastWarmedKeyRef.current = key;
@@ -78,6 +86,10 @@ export function useWarmTask({
7886
runtime_adapter: warmRuntimeAdapter,
7987
model: warmModel,
8088
reasoning_effort: warmReasoningEffort,
89+
...(warmSandboxEnvironmentId
90+
? { sandbox_environment_id: warmSandboxEnvironmentId }
91+
: {}),
92+
...(warmCustomImageId ? { custom_image_id: warmCustomImageId } : {}),
8193
}).catch((error) => {
8294
lastWarmedKeyRef.current = null;
8395
log.warn("Failed to warm task", error);
@@ -94,5 +106,7 @@ export function useWarmTask({
94106
normalizedRuntimeAdapter,
95107
normalizedModel,
96108
normalizedReasoningEffort,
109+
normalizedSandboxEnvironmentId,
110+
normalizedCustomImageId,
97111
]);
98112
}

0 commit comments

Comments
 (0)