Skip to content

Commit dffec58

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(pi): bound babysit round setup
1 parent a38c682 commit dffec58

2 files changed

Lines changed: 100 additions & 19 deletions

File tree

apps/sim/executor/handlers/pi/babysit-backend.test.ts

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ function params(overrides: Partial<PiBabysitRunParams> = {}): PiBabysitRunParams
142142
pullNumber: 7,
143143
maxRounds: 3,
144144
reviewMentions: [],
145-
executionBudgetMs: 20 * 60 * 1000,
145+
executionBudgetMs: 40 * 60 * 1000,
146146
...overrides,
147147
}
148148
}
@@ -156,15 +156,23 @@ function makeRunner(options: {
156156
pushResult?: ReturnType<typeof commandResult>
157157
roundFile?: string
158158
}) {
159-
const runCalls: Array<{ command: string; envs?: Record<string, string> }> = []
159+
const runCalls: Array<{
160+
command: string
161+
envs?: Record<string, string>
162+
timeoutMs?: number
163+
}> = []
160164
let prepareCall = 0
161165
const runner = {
162166
run: vi.fn(
163167
async (
164168
command: string,
165-
runOptions: { envs?: Record<string, string>; onStdout?: (chunk: string) => void }
169+
runOptions: {
170+
envs?: Record<string, string>
171+
onStdout?: (chunk: string) => void
172+
timeoutMs?: number
173+
}
166174
) => {
167-
runCalls.push({ command, envs: runOptions.envs })
175+
runCalls.push({ command, envs: runOptions.envs, timeoutMs: runOptions.timeoutMs })
168176
if (command.includes('git clone')) {
169177
return commandResult('__GIT_CONFIG_DIGEST__=digest-1\n')
170178
}
@@ -253,6 +261,40 @@ describe('runBabysitPiWithOptions', () => {
253261
expect(mockWithPiSandbox).not.toHaveBeenCalled()
254262
})
255263

264+
it('refuses excess failing checks before fetching discarded diagnostics', async () => {
265+
const failures = Array.from({ length: 21 }, (_, index) => ({
266+
...failingCheck,
267+
key: `check:ci-${index}`,
268+
name: `ci-${index}`,
269+
}))
270+
mockFetchSnapshot.mockResolvedValue(snapshot)
271+
mockFetchThreads.mockResolvedValue({
272+
actionable: [],
273+
skipped: [],
274+
totalUnresolved: 0,
275+
latestReview: null,
276+
})
277+
mockFetchChecks.mockResolvedValue({
278+
...failingChecks,
279+
checks: failures,
280+
failing: failures,
281+
blockingFailing: failures,
282+
contextRequirements: new Map(failures.map((check) => [check.key, true])),
283+
})
284+
const { runner, runCalls } = makeRunner({})
285+
mockWithPiSandbox.mockImplementation(async (callback) => callback(runner))
286+
287+
const result = await runBabysitPiWithOptions(params(), { onEvent: vi.fn() })
288+
289+
expect(result).toMatchObject({
290+
stopReason: 'bounds_exceeded',
291+
rounds: 0,
292+
commitsPushed: 0,
293+
})
294+
expect(mockFetchDiagnostics).not.toHaveBeenCalled()
295+
expect(runCalls.some(({ command }) => command.includes('pi -p --mode json'))).toBe(false)
296+
})
297+
256298
it('advances the pin after one exact hardened push and resolves the round', async () => {
257299
mockFetchSnapshot
258300
.mockResolvedValueOnce(snapshot)
@@ -274,14 +316,22 @@ describe('runBabysitPiWithOptions', () => {
274316
})
275317
mockFetchChecks.mockResolvedValueOnce(failingChecks).mockResolvedValueOnce(greenChecks)
276318

277-
const runCalls: Array<{ command: string; envs?: Record<string, string> }> = []
319+
const runCalls: Array<{
320+
command: string
321+
envs?: Record<string, string>
322+
timeoutMs?: number
323+
}> = []
278324
const runner = {
279325
run: vi.fn(
280326
async (
281327
command: string,
282-
options: { envs?: Record<string, string>; onStdout?: (chunk: string) => void }
328+
options: {
329+
envs?: Record<string, string>
330+
onStdout?: (chunk: string) => void
331+
timeoutMs?: number
332+
}
283333
) => {
284-
runCalls.push({ command, envs: options.envs })
334+
runCalls.push({ command, envs: options.envs, timeoutMs: options.timeoutMs })
285335
if (command.includes('git clone')) {
286336
return commandResult('__GIT_CONFIG_DIGEST__=digest-1\n')
287337
}
@@ -331,6 +381,8 @@ describe('runBabysitPiWithOptions', () => {
331381
expect(piCall?.command).toContain(
332382
'--no-extensions --no-prompt-templates --no-skills --no-approve'
333383
)
384+
expect(piCall?.timeoutMs).toBeGreaterThan(19 * 60 * 1000)
385+
expect(piCall?.timeoutMs).toBeLessThanOrEqual(20 * 60 * 1000)
334386
expect(piCall?.envs).not.toHaveProperty('GITHUB_TOKEN')
335387
const pushCall = runCalls.find(({ command }) => command.includes('CURRENT_DIGEST='))
336388
expect(pushCall?.command.indexOf('CURRENT_DIGEST=')).toBeLessThan(

apps/sim/executor/handlers/pi/babysit-backend.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const MAX_FAILING_CHECKS_IN_PROMPT = 20
8888
const MAX_REVIEW_PROMPT_BYTES = 250_000
8989
const MAX_CHECK_PROMPT_BYTES = 400_000
9090
const MIN_ROUND_BUDGET_MS = 5 * 60 * 1000
91+
const ROUND_FINALIZATION_RESERVE_MS = 2 * FINALIZE_TIMEOUT_MS
9192

9293
const BABYSIT_GUIDANCE =
9394
'You are fixing an existing pull request in a long-lived automated sandbox. Make only minimal ' +
@@ -248,12 +249,6 @@ function buildRoundPrompt(
248249
detailsUrl: check.detailsUrl,
249250
diagnostics: diagnostics.get(check.key),
250251
}))
251-
if (checks.failing.length > MAX_FAILING_CHECKS_IN_PROMPT) {
252-
throw new BabysitGitHubError(
253-
'bounds_exceeded',
254-
`Babysit found ${checks.failing.length} failing checks; at most ${MAX_FAILING_CHECKS_IN_PROMPT} fit in one trusted round.`
255-
)
256-
}
257252
const reviewJson = untrustedJson(reviewPayload)
258253
const checkJson = untrustedJson(checkPayload)
259254
if (new TextEncoder().encode(reviewJson).byteLength > MAX_REVIEW_PROMPT_BYTES) {
@@ -340,7 +335,8 @@ async function runRoundAgent(
340335
signal: AbortSignal,
341336
prompt: string,
342337
secrets: readonly string[],
343-
keyEnvVar: string
338+
keyEnvVar: string,
339+
timeoutMs: number
344340
): Promise<PiRunTotals> {
345341
await raceAbort(
346342
runner.run(`rm -f ${BABYSIT_ROUND_PATH}`, { timeoutMs: FINALIZE_TIMEOUT_MS }),
@@ -379,7 +375,7 @@ async function runRoundAgent(
379375
}
380376
: {}),
381377
},
382-
timeoutMs: PI_TIMEOUT_MS,
378+
timeoutMs,
383379
onStdout: handleChunk,
384380
}
385381
),
@@ -656,7 +652,10 @@ export async function runBabysitPiWithOptions(
656652
latestThreads!.actionable.length > 0 || latestChecks!.blockingFailing.length > 0
657653
if (!needsAgent) {
658654
const remaining = lifetime - (Date.now() - startedAt)
659-
if (remaining <= options.roundWaitMs + MIN_ROUND_BUDGET_MS) {
655+
if (
656+
remaining <=
657+
options.roundWaitMs + MIN_ROUND_BUDGET_MS + ROUND_FINALIZATION_RESERVE_MS
658+
) {
660659
const reason = outstandingReason(
661660
'budget_exhausted',
662661
latestThreads!,
@@ -699,7 +698,10 @@ export async function runBabysitPiWithOptions(
699698
)
700699
return resultFor(totals, reason, progress, threadsClean, latestChecks!.checksGreen)
701700
}
702-
if (Date.now() - startedAt + MIN_ROUND_BUDGET_MS > lifetime) {
701+
if (
702+
Date.now() - startedAt + MIN_ROUND_BUDGET_MS + ROUND_FINALIZATION_RESERVE_MS >
703+
lifetime
704+
) {
703705
const reason = outstandingReason(
704706
'budget_exhausted',
705707
latestThreads!,
@@ -708,6 +710,18 @@ export async function runBabysitPiWithOptions(
708710
)
709711
return resultFor(totals, reason, progress, threadsClean, latestChecks!.checksGreen)
710712
}
713+
if (latestChecks!.failing.length > MAX_FAILING_CHECKS_IN_PROMPT) {
714+
progress.notes.push(
715+
`Babysit found ${latestChecks!.failing.length} failing checks; at most ${MAX_FAILING_CHECKS_IN_PROMPT} fit in one trusted round.`
716+
)
717+
return resultFor(
718+
totals,
719+
'bounds_exceeded',
720+
progress,
721+
threadsClean,
722+
latestChecks!.checksGreen
723+
)
724+
}
711725

712726
const diagnostics = await fetchBabysitCheckDiagnostics(
713727
params,
@@ -716,6 +730,19 @@ export async function runBabysitPiWithOptions(
716730
signal
717731
)
718732
const prompt = buildRoundPrompt(params, latestThreads!, latestChecks!, diagnostics, secrets)
733+
const agentTimeoutMs = Math.min(
734+
PI_TIMEOUT_MS,
735+
lifetime - (Date.now() - startedAt) - ROUND_FINALIZATION_RESERVE_MS
736+
)
737+
if (agentTimeoutMs < MIN_ROUND_BUDGET_MS) {
738+
const reason = outstandingReason(
739+
'budget_exhausted',
740+
latestThreads!,
741+
latestChecks!,
742+
awaitingReview
743+
)
744+
return resultFor(totals, reason, progress, threadsClean, latestChecks!.checksGreen)
745+
}
719746
progress.rounds += 1
720747
context.onEvent({ type: 'text', text: `Babysit round ${progress.rounds} started.\n` })
721748
try {
@@ -726,7 +753,8 @@ export async function runBabysitPiWithOptions(
726753
signal,
727754
prompt,
728755
secrets,
729-
keyEnvVar
756+
keyEnvVar,
757+
agentTimeoutMs
730758
)
731759
mergeRoundTotals(totals, roundTotals)
732760
} catch (error) {
@@ -926,7 +954,8 @@ export async function runBabysitPiWithOptions(
926954
const remainingBeforeWait = lifetime - (Date.now() - startedAt)
927955
if (
928956
options.roundWaitMs > 0 &&
929-
remainingBeforeWait > options.roundWaitMs + MIN_ROUND_BUDGET_MS
957+
remainingBeforeWait >
958+
options.roundWaitMs + MIN_ROUND_BUDGET_MS + ROUND_FINALIZATION_RESERVE_MS
930959
) {
931960
await sleepUntilAborted(options.roundWaitMs, signal)
932961
if (signal.aborted) throw new Error('Pi run aborted')

0 commit comments

Comments
 (0)