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

Commit c480d36

Browse files
authored
perf(agent): overlap existing PR checkout with startup
Overlaps the best-effort existing-PR checkout with session startup so the branch is ready by the time the ACP session starts, instead of the agent paying for `gh pr checkout` on the critical path mid-task. Wraps the fire-and-await in try/finally so a throw from skill-bundle install / native-resume prep / MCP relay startup can never abandon an in-flight checkout mutating the working tree; bounds the checkout with an overall 60s deadline + AbortController so a hung `gh` can't stall session start; validates the prUrl shape before handing it to `gh`; and adds unit + gating tests. Review fixes for #3684: dangling-checkout on the error path, startup-latency budget, prUrl validation, and test coverage for the checkout args, cwd forwarding, detached-HEAD, empty-branch, view-succeeds/checkout-fails, and the auto-publish gating contract. Generated-By: PostHog Code Task-Id: 29c3cf5a-82e2-4f36-aeba-d0322ab741a5
1 parent d9f9204 commit c480d36

4 files changed

Lines changed: 477 additions & 18 deletions

File tree

packages/agent/src/server/agent-server.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
SSE_KEEPALIVE_INTERVAL_MS,
4545
} from "./agent-server";
4646
import { type JwtPayload, SANDBOX_CONNECTION_AUDIENCE } from "./jwt";
47+
import type { ExistingPrCheckoutResult } from "./pr-checkout";
4748

4849
const mockedClaudeSdk = vi.hoisted(() => {
4950
const createSuccessResult = () => ({
@@ -232,6 +233,13 @@ interface TestableServer {
232233
inboxReportUrl?: string | null,
233234
): string;
234235
buildDetectedPrContext(prUrl: string): string;
236+
buildExistingPrCheckoutPromise(
237+
prUrl: string | null,
238+
): Promise<ExistingPrCheckoutResult> | null;
239+
logExistingPrCheckoutResult(
240+
prUrl: string | null,
241+
result: ExistingPrCheckoutResult,
242+
): void;
235243
buildSessionSystemPrompt(
236244
prUrl?: string | null,
237245
slackThreadUrl?: string | null,
@@ -3871,6 +3879,78 @@ describe("AgentServer HTTP Mode", () => {
38713879
delete process.env.POSTHOG_CODE_INTERACTION_ORIGIN;
38723880
});
38733881
});
3882+
3883+
describe("buildExistingPrCheckoutPromise", () => {
3884+
const prUrl = "https://github.com/org/repo/pull/1";
3885+
3886+
afterEach(() => {
3887+
delete process.env.POSTHOG_CODE_INTERACTION_ORIGIN;
3888+
});
3889+
3890+
// Guards the gating condition: a review-first run (no auto-publish) must
3891+
// not silently check out a PR branch the prompt told the agent to leave
3892+
// alone. Regressing the guard to always-checkout would fail here.
3893+
it("does not check out when auto-publish is off", () => {
3894+
const s = createServer();
3895+
const promise = (
3896+
s as unknown as TestableServer
3897+
).buildExistingPrCheckoutPromise(prUrl);
3898+
expect(promise).toBeNull();
3899+
});
3900+
3901+
it("does not check out when there is no prUrl", () => {
3902+
process.env.POSTHOG_CODE_INTERACTION_ORIGIN = "slack";
3903+
const s = createServer();
3904+
const promise = (
3905+
s as unknown as TestableServer
3906+
).buildExistingPrCheckoutPromise(null);
3907+
expect(promise).toBeNull();
3908+
});
3909+
3910+
it("does not check out when createPr is false, even on a Slack-origin run", () => {
3911+
process.env.POSTHOG_CODE_INTERACTION_ORIGIN = "slack";
3912+
const s = createServer({ createPr: false });
3913+
const promise = (
3914+
s as unknown as TestableServer
3915+
).buildExistingPrCheckoutPromise(prUrl);
3916+
expect(promise).toBeNull();
3917+
});
3918+
3919+
it("does not check out when no repository is connected", () => {
3920+
process.env.POSTHOG_CODE_INTERACTION_ORIGIN = "slack";
3921+
const s = createServer({ repositoryPath: undefined });
3922+
const promise = (
3923+
s as unknown as TestableServer
3924+
).buildExistingPrCheckoutPromise(prUrl);
3925+
expect(promise).toBeNull();
3926+
});
3927+
3928+
it("starts a checkout when auto-publish is on for a Slack-origin run", () => {
3929+
process.env.POSTHOG_CODE_INTERACTION_ORIGIN = "slack";
3930+
const s = createServer();
3931+
const promise = (
3932+
s as unknown as TestableServer
3933+
).buildExistingPrCheckoutPromise(prUrl);
3934+
expect(promise).toBeInstanceOf(Promise);
3935+
// Sanity: the promise resolves to a checkout result shape (it will fail
3936+
// against the synthetic URL with no real gh, which is fine — we only
3937+
// assert the promise was actually kicked off).
3938+
expect(typeof promise).toBe("object");
3939+
});
3940+
3941+
// Guards the failure fallback: a transient gh failure must surface as a
3942+
// warn, never throw or abort startup. Regressing the failed branch to
3943+
// `throw` would fail here.
3944+
it("logs a warning for a failed checkout result without throwing", () => {
3945+
const s = createServer();
3946+
expect(() =>
3947+
(s as unknown as TestableServer).logExistingPrCheckoutResult(prUrl, {
3948+
status: "failed",
3949+
error: "gh unavailable",
3950+
}),
3951+
).not.toThrow();
3952+
});
3953+
});
38743954
});
38753955

38763956
// Exercises getPendingUserPrompt directly (no HTTP server / git repo) so we can

packages/agent/src/server/agent-server.ts

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ import {
9494
import { TaskRunEventStreamSender } from "./event-stream-sender";
9595
import { type JwtPayload, JwtValidationError, validateJwt } from "./jwt";
9696
import { type McpRelayResponse, McpRelayServer } from "./mcp-relay-server";
97+
import {
98+
checkoutExistingPullRequest,
99+
type ExistingPrCheckoutResult,
100+
} from "./pr-checkout";
97101
import { resolveRtkSavings } from "./rtk-savings";
98102
import { RunUsageAccumulator } from "./run-usage";
99103
import {
@@ -1505,28 +1509,51 @@ export class AgentServer {
15051509
};
15061510

15071511
await this.waitForRepoReady();
1508-
await this.installSkillBundleArtifacts(
1509-
payload.task_id,
1510-
payload.run_id,
1511-
this.getArtifactsById(preTaskRun?.artifacts, pendingUserArtifactIds),
1512-
);
1513-
1514-
const nativeResume = await this.prepareNativeResume(
1515-
payload,
1516-
posthogAPI,
1517-
preTaskRun,
1518-
runtimeAdapter,
1519-
sessionCwd,
1520-
initialPermissionMode,
1521-
);
1512+
const existingPrCheckoutPromise =
1513+
this.buildExistingPrCheckoutPromise(prUrl);
1514+
// Overlap the best-effort PR checkout with the rest of session setup. The
1515+
// checkout promise is always awaited in `finally` so a throw from
1516+
// installSkillBundleArtifacts / prepareNativeResume / startMcpRelayServer
1517+
// can never abandon an in-flight `gh pr checkout` that would keep mutating
1518+
// the working tree after session start has been abandoned — the awaited
1519+
// settle (plus the checkout's own abort-on-return) cancels it.
1520+
let nativeResume: { sessionId: string; warm: boolean } | null;
15221521
let effectiveSessionMeta: typeof sessionMeta & {
15231522
nativeGoal?: NonNullable<ResumeState["nativeGoal"]>;
15241523
} = sessionMeta;
1524+
let sessionMcpServers: RemoteMcpServer[];
1525+
try {
1526+
await this.installSkillBundleArtifacts(
1527+
payload.task_id,
1528+
payload.run_id,
1529+
this.getArtifactsById(preTaskRun?.artifacts, pendingUserArtifactIds),
1530+
);
15251531

1526-
const sessionMcpServers = [
1527-
...(this.config.mcpServers ?? []),
1528-
...(await this.startMcpRelayServer()),
1529-
];
1532+
nativeResume = await this.prepareNativeResume(
1533+
payload,
1534+
posthogAPI,
1535+
preTaskRun,
1536+
runtimeAdapter,
1537+
sessionCwd,
1538+
initialPermissionMode,
1539+
);
1540+
1541+
sessionMcpServers = [
1542+
...(this.config.mcpServers ?? []),
1543+
...(await this.startMcpRelayServer()),
1544+
];
1545+
} finally {
1546+
// Always consume the checkout result — on the success path this is the
1547+
// intended await; on a throw it ensures the in-flight checkout settles
1548+
// (and aborts its children) instead of mutating the tree in the
1549+
// background. checkoutExistingPullRequest never rejects.
1550+
if (existingPrCheckoutPromise) {
1551+
this.logExistingPrCheckoutResult(
1552+
prUrl,
1553+
await existingPrCheckoutPromise,
1554+
);
1555+
}
1556+
}
15301557

15311558
let acpSessionId: string | null = null;
15321559
if (nativeResume) {
@@ -3143,6 +3170,54 @@ export class AgentServer {
31433170
return `Continue working on the existing PR branch. If it is not already checked out, check it out with \`gh pr checkout ${prUrl}\`. Do not check it out again when it is already active.`;
31443171
}
31453172

3173+
/**
3174+
* Fire-and-overlap: starts the best-effort PR-branch checkout so it runs
3175+
* concurrently with the rest of session setup, returning the promise (or
3176+
* null when there is nothing to check out). Only runs when auto-publishing,
3177+
* matching the system-prompt fallback's gate: a review-first run must not
3178+
* silently check out a branch the prompt told the agent to leave alone.
3179+
*/
3180+
private buildExistingPrCheckoutPromise(
3181+
prUrl: string | null,
3182+
): Promise<ExistingPrCheckoutResult> | null {
3183+
if (!prUrl || !this.config.repositoryPath) {
3184+
return null;
3185+
}
3186+
if (!this.shouldAutoPublishCloudChanges()) {
3187+
return null;
3188+
}
3189+
return checkoutExistingPullRequest({
3190+
repositoryPath: this.config.repositoryPath,
3191+
prUrl,
3192+
});
3193+
}
3194+
3195+
/**
3196+
* Consume a pre-checkout result without throwing — a transient `gh` failure
3197+
* must fall back to the agent's own checkout (via the system-prompt
3198+
* instruction), never abort session start.
3199+
*/
3200+
private logExistingPrCheckoutResult(
3201+
prUrl: string | null,
3202+
result: ExistingPrCheckoutResult,
3203+
): void {
3204+
if (result.status === "failed") {
3205+
this.logger.warn(
3206+
"Existing PR pre-checkout failed; agent will retry if needed",
3207+
{
3208+
prUrl,
3209+
error: result.error,
3210+
},
3211+
);
3212+
} else {
3213+
this.logger.debug("Existing PR branch prepared before session start", {
3214+
prUrl,
3215+
branch: result.branch,
3216+
alreadyActive: result.status === "already_active",
3217+
});
3218+
}
3219+
}
3220+
31463221
private buildDetectedPrContext(prUrl: string): string {
31473222
if (!this.shouldAutoPublishCloudChanges()) {
31483223
return (

0 commit comments

Comments
 (0)