Skip to content

Commit ecaa9f0

Browse files
Merging 496e683 into trunk-temp/pr-3481/94f6a581-6a94-45ce-adcc-124c6fda40e4
2 parents b98458e + 496e683 commit ecaa9f0

10 files changed

Lines changed: 203 additions & 1 deletion

File tree

packages/api-client/src/posthog-client.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,39 @@ describe("PostHogAPIClient", () => {
544544
);
545545
});
546546

547+
it("forwards the selected sandbox environment and custom image", async () => {
548+
const fetch = vi.fn().mockResolvedValue({
549+
ok: true,
550+
text: async () =>
551+
JSON.stringify({ task_id: "task-1", run_id: "run-1" }),
552+
});
553+
const client = makeClient(fetch);
554+
555+
await client.warmTask({
556+
repository: "PostHog/posthog",
557+
github_integration: 42,
558+
sandbox_environment_id: "environment-123",
559+
custom_image_id: "image-123",
560+
});
561+
562+
expect(fetch).toHaveBeenCalledWith(
563+
expect.objectContaining({
564+
overrides: {
565+
body: JSON.stringify({
566+
repository: "PostHog/posthog",
567+
github_integration: 42,
568+
branch: null,
569+
runtime_adapter: null,
570+
model: null,
571+
reasoning_effort: null,
572+
sandbox_environment_id: "environment-123",
573+
custom_image_id: "image-123",
574+
}),
575+
},
576+
}),
577+
);
578+
});
579+
547580
it("sends a null branch when none is provided", async () => {
548581
const fetch = vi.fn().mockResolvedValue({
549582
ok: true,

packages/api-client/src/posthog-client.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2817,6 +2817,8 @@ export class PostHogAPIClient {
28172817
runtime_adapter?: string | null;
28182818
model?: string | null;
28192819
reasoning_effort?: string | null;
2820+
sandbox_environment_id?: string | null;
2821+
custom_image_id?: string | null;
28202822
}): Promise<{ task_id: string; run_id: string } | null> {
28212823
const teamId = await this.getTeamId();
28222824
const urlPath = `/api/projects/${teamId}/tasks/warm/`;
@@ -2833,6 +2835,12 @@ export class PostHogAPIClient {
28332835
runtime_adapter: options.runtime_adapter ?? null,
28342836
model: options.model ?? null,
28352837
reasoning_effort: options.reasoning_effort ?? null,
2838+
...(options.sandbox_environment_id
2839+
? { sandbox_environment_id: options.sandbox_environment_id }
2840+
: {}),
2841+
...(options.custom_image_id
2842+
? { custom_image_id: options.custom_image_id }
2843+
: {}),
28362844
}),
28372845
},
28382846
});

packages/core/src/task-detail/taskCreationHost.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export interface ITaskCreationHost {
125125
runtimeAdapter?: string | null;
126126
model?: string | null;
127127
reasoningEffort?: string | null;
128+
sandboxEnvironmentId?: string | null;
129+
customImageId?: string | null;
128130
}): { taskId: string; runId: string } | null;
129131
uploadRunAttachments(
130132
client: TaskCreationApiClient,

packages/core/src/task-detail/taskCreationSaga.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,8 @@ describe("TaskCreationSaga", () => {
569569
runtimeAdapter: null,
570570
model: null,
571571
reasoningEffort: null,
572+
sandboxEnvironmentId: null,
573+
customImageId: null,
572574
});
573575
// The bundle must land on the warm run before createTask triggers activation.
574576
expect(mockHost.uploadRunAttachments).toHaveBeenCalledWith(
@@ -687,6 +689,83 @@ describe("TaskCreationSaga", () => {
687689
});
688690
});
689691

692+
it.each([
693+
{
694+
selection: "sandbox environment",
695+
input: { sandboxEnvironmentId: "environment-123" },
696+
expectedRunOptions: { sandboxEnvironmentId: "environment-123" },
697+
},
698+
{
699+
selection: "custom image",
700+
input: { customImageId: "image-123" },
701+
expectedRunOptions: { customImageId: "image-123" },
702+
},
703+
])(
704+
"falls back to a cold run without a matching warm $selection lease",
705+
async ({ input, expectedRunOptions }) => {
706+
mockHost.takeWarmTaskLease.mockReturnValue(null);
707+
const createdTask = createTask();
708+
const startedTask = createTask({ latest_run: createRun() });
709+
const createTaskMock = vi.fn().mockResolvedValue(createdTask);
710+
const createTaskRunMock = vi.fn().mockResolvedValue(createRun());
711+
const startTaskRunMock = vi.fn().mockResolvedValue(startedTask);
712+
const saga = makeSaga({
713+
createTask: createTaskMock,
714+
createTaskRun: createTaskRunMock,
715+
startTaskRun: startTaskRunMock,
716+
});
717+
718+
const result = await saga.run({
719+
content: "Ship the fix",
720+
repository: "posthog/posthog",
721+
workspaceMode: "cloud",
722+
branch: "main",
723+
...input,
724+
});
725+
726+
expect(result.success).toBe(true);
727+
expect(createTaskMock.mock.calls[0][0].branch).toBeUndefined();
728+
expect(createTaskRunMock).toHaveBeenCalledWith(
729+
"task-123",
730+
expect.objectContaining(expectedRunOptions),
731+
);
732+
},
733+
);
734+
735+
it("reuses a warm run built from the selected custom image", async () => {
736+
mockHost.takeWarmTaskLease.mockReturnValue({
737+
taskId: "warm-task",
738+
runId: "warm-run",
739+
});
740+
const warmActivatedTask = createTask({
741+
id: "warm-task",
742+
latest_run: createRun({ id: "warm-run", task: "warm-task" }),
743+
});
744+
const createTaskMock = vi.fn().mockResolvedValue(warmActivatedTask);
745+
const createTaskRunMock = vi.fn();
746+
const saga = makeSaga({
747+
createTask: createTaskMock,
748+
createTaskRun: createTaskRunMock,
749+
});
750+
751+
const result = await saga.run({
752+
content: "Ship the fix",
753+
repository: "posthog/posthog",
754+
workspaceMode: "cloud",
755+
branch: "main",
756+
customImageId: "image-123",
757+
});
758+
759+
expect(result.success).toBe(true);
760+
expect(createTaskMock).toHaveBeenCalledWith(
761+
expect.objectContaining({
762+
branch: "main",
763+
custom_image_id: "image-123",
764+
}),
765+
);
766+
expect(createTaskRunMock).not.toHaveBeenCalled();
767+
});
768+
690769
it("uses the selected user GitHub integration for cloud task creation", async () => {
691770
const createdTask = createTask({
692771
github_user_integration: "user-integration-123",

packages/core/src/task-detail/taskCreationSaga.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,13 +692,22 @@ export class TaskCreationSaga extends Saga<
692692
runtimeAdapter: input.adapter ?? null,
693693
model: input.model ?? null,
694694
reasoningEffort: input.reasoningLevel ?? null,
695+
sandboxEnvironmentId: input.sandboxEnvironmentId ?? null,
696+
customImageId: input.customImageId ?? null,
695697
})
696698
: null;
697699

700+
const requiresConfiguredWarm = Boolean(
701+
input.sandboxEnvironmentId || input.customImageId,
702+
);
703+
698704
const needsAttachments =
699705
transport.filePaths.length > 0 || transport.skillBundles.length > 0;
700706
if (!needsAttachments) {
701-
return base;
707+
return {
708+
...base,
709+
suppressWarmReuse: requiresConfiguredWarm && !lease,
710+
};
702711
}
703712
if (!lease) {
704713
return { ...base, suppressWarmReuse: true };
@@ -788,6 +797,14 @@ export class TaskCreationSaga extends Saga<
788797
input.workspaceMode === "cloud"
789798
? (input.reasoningLevel ?? null)
790799
: undefined,
800+
sandbox_environment_id:
801+
input.workspaceMode === "cloud" && !warmPayload?.suppressWarmReuse
802+
? input.sandboxEnvironmentId
803+
: undefined,
804+
custom_image_id:
805+
input.workspaceMode === "cloud" && !warmPayload?.suppressWarmReuse
806+
? input.customImageId
807+
: undefined,
791808
signal_report: input.signalReportId ?? undefined,
792809
channel: input.channelId ?? undefined,
793810
pending_user_message: warmPayload?.pendingUserMessage,

packages/ui/src/features/task-detail/components/TaskInput.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,8 @@ export function TaskInput({
730730
runtimeAdapter: adapter ?? null,
731731
model: effectiveModel,
732732
reasoningEffort: effectiveReasoningLevel,
733+
sandboxEnvironmentId: workspaceMode === "cloud" ? selectedCloudEnvId : null,
734+
customImageId: workspaceMode === "cloud" ? selectedCustomImageId : null,
733735
});
734736

735737
const branchForTaskCreation =

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ interface Props {
2828
runtimeAdapter?: string | null;
2929
model?: string | null;
3030
reasoningEffort?: string | null;
31+
sandboxEnvironmentId?: string | null;
32+
customImageId?: string | null;
3133
}
3234

3335
const cloudTyping: Props = {
@@ -201,6 +203,41 @@ describe("useWarmTask", () => {
201203
expect(mockClient.warmTask).toHaveBeenCalledTimes(2);
202204
});
203205

206+
it("forwards sandbox configuration and re-warms when the image changes", async () => {
207+
const { rerender } = renderHook((props: Props) => useWarmTask(props), {
208+
initialProps: {
209+
...cloudTyping,
210+
sandboxEnvironmentId: "environment-123",
211+
customImageId: "image-123",
212+
},
213+
});
214+
await flushDebounce();
215+
expect(mockClient.warmTask).toHaveBeenLastCalledWith({
216+
repository: "acme/repo",
217+
github_integration: 42,
218+
branch: "main",
219+
...NULL_RUNTIME,
220+
sandbox_environment_id: "environment-123",
221+
custom_image_id: "image-123",
222+
});
223+
224+
rerender({
225+
...cloudTyping,
226+
sandboxEnvironmentId: "environment-123",
227+
customImageId: "image-456",
228+
});
229+
await flushDebounce();
230+
expect(mockClient.warmTask).toHaveBeenLastCalledWith({
231+
repository: "acme/repo",
232+
github_integration: 42,
233+
branch: "main",
234+
...NULL_RUNTIME,
235+
sandbox_environment_id: "environment-123",
236+
custom_image_id: "image-456",
237+
});
238+
expect(mockClient.warmTask).toHaveBeenCalledTimes(2);
239+
});
240+
204241
it("warms again for a new selection after a failed warm", async () => {
205242
mockClient.warmTask.mockRejectedValueOnce(new Error("boom"));
206243
const { rerender } = renderHook((props: Props) => useWarmTask(props), {

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ interface UseWarmTaskOptions {
2121
runtimeAdapter?: string | null;
2222
model?: string | null;
2323
reasoningEffort?: string | null;
24+
sandboxEnvironmentId?: string | null;
25+
customImageId?: string | null;
2426
}
2527

2628
export function useWarmTask({
@@ -32,6 +34,8 @@ export function useWarmTask({
3234
runtimeAdapter,
3335
model,
3436
reasoningEffort,
37+
sandboxEnvironmentId,
38+
customImageId,
3539
}: UseWarmTaskOptions): void {
3640
const enabled = useFeatureFlag(TASKS_PREWARM_SANDBOX_FLAG);
3741
const client = useOptionalAuthenticatedClient();
@@ -44,6 +48,8 @@ export function useWarmTask({
4448
const normalizedRuntimeAdapter = runtimeAdapter ?? null;
4549
const normalizedModel = model ?? null;
4650
const normalizedReasoningEffort = reasoningEffort ?? null;
51+
const normalizedSandboxEnvironmentId = sandboxEnvironmentId ?? null;
52+
const normalizedCustomImageId = customImageId ?? null;
4753
const eligible =
4854
enabled &&
4955
isCloud &&
@@ -59,6 +65,8 @@ export function useWarmTask({
5965
runtimeAdapter: normalizedRuntimeAdapter,
6066
model: normalizedModel,
6167
reasoningEffort: normalizedReasoningEffort,
68+
sandboxEnvironmentId: normalizedSandboxEnvironmentId,
69+
customImageId: normalizedCustomImageId,
6270
})}`
6371
: null;
6472

@@ -84,6 +92,8 @@ export function useWarmTask({
8492
const warmRuntimeAdapter = normalizedRuntimeAdapter;
8593
const warmModel = normalizedModel;
8694
const warmReasoningEffort = normalizedReasoningEffort;
95+
const warmSandboxEnvironmentId = normalizedSandboxEnvironmentId;
96+
const warmCustomImageId = normalizedCustomImageId;
8797
debounceRef.current = setTimeout(() => {
8898
debounceRef.current = null;
8999
lastWarmedKeyRef.current = key;
@@ -95,6 +105,10 @@ export function useWarmTask({
95105
runtime_adapter: warmRuntimeAdapter,
96106
model: warmModel,
97107
reasoning_effort: warmReasoningEffort,
108+
...(warmSandboxEnvironmentId
109+
? { sandbox_environment_id: warmSandboxEnvironmentId }
110+
: {}),
111+
...(warmCustomImageId ? { custom_image_id: warmCustomImageId } : {}),
98112
})
99113
.then((warm) => {
100114
if (warm) {
@@ -105,6 +119,8 @@ export function useWarmTask({
105119
runtimeAdapter: warmRuntimeAdapter,
106120
model: warmModel,
107121
reasoningEffort: warmReasoningEffort,
122+
sandboxEnvironmentId: warmSandboxEnvironmentId,
123+
customImageId: warmCustomImageId,
108124
}),
109125
{ taskId: warm.task_id, runId: warm.run_id },
110126
);
@@ -127,5 +143,7 @@ export function useWarmTask({
127143
normalizedRuntimeAdapter,
128144
normalizedModel,
129145
normalizedReasoningEffort,
146+
normalizedSandboxEnvironmentId,
147+
normalizedCustomImageId,
130148
]);
131149
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export interface WarmTaskLeaseKeyParts {
99
runtimeAdapter?: string | null;
1010
model?: string | null;
1111
reasoningEffort?: string | null;
12+
sandboxEnvironmentId?: string | null;
13+
customImageId?: string | null;
1214
}
1315

1416
export function buildWarmTaskLeaseKey(parts: WarmTaskLeaseKeyParts): string {
@@ -18,6 +20,8 @@ export function buildWarmTaskLeaseKey(parts: WarmTaskLeaseKeyParts): string {
1820
parts.runtimeAdapter ?? "",
1921
parts.model ?? "",
2022
parts.reasoningEffort ?? "",
23+
parts.sandboxEnvironmentId ?? "",
24+
parts.customImageId ?? "",
2125
].join(":");
2226
}
2327

packages/ui/src/features/task-detail/taskCreationHostImpl.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ export class TrpcTaskCreationHost implements ITaskCreationHost {
162162
runtimeAdapter?: string | null;
163163
model?: string | null;
164164
reasoningEffort?: string | null;
165+
sandboxEnvironmentId?: string | null;
166+
customImageId?: string | null;
165167
}): { taskId: string; runId: string } | null {
166168
return takeWarmTaskLease(args);
167169
}

0 commit comments

Comments
 (0)