Skip to content

Commit 770ca81

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(pi): keep babysit sandboxes active
1 parent dffec58 commit 770ca81

4 files changed

Lines changed: 43 additions & 11 deletions

File tree

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
mockRequestReview,
1414
mockReviewLanded,
1515
mockResolvePiSandboxLifetime,
16+
mockSleepUntilAborted,
1617
} = vi.hoisted(() => ({
1718
mockWithPiSandbox: vi.fn(),
1819
mockFetchSnapshot: vi.fn(),
@@ -23,6 +24,7 @@ const {
2324
mockRequestReview: vi.fn(),
2425
mockReviewLanded: vi.fn(),
2526
mockResolvePiSandboxLifetime: vi.fn(),
27+
mockSleepUntilAborted: vi.fn(),
2628
}))
2729

2830
vi.mock('@/lib/execution/remote-sandbox', () => ({
@@ -32,6 +34,9 @@ vi.mock('@/lib/execution/cancellation', () => ({
3234
isRedisCancellationEnabled: () => false,
3335
isExecutionCancelled: vi.fn().mockResolvedValue(false),
3436
}))
37+
vi.mock('@/lib/data-drains/destinations/utils', () => ({
38+
sleepUntilAborted: mockSleepUntilAborted,
39+
}))
3540
vi.mock('@/lib/execution/remote-sandbox/pi-lifetime', async (importOriginal) => {
3641
const original =
3742
await importOriginal<typeof import('@/lib/execution/remote-sandbox/pi-lifetime')>()
@@ -218,6 +223,7 @@ describe('runBabysitPiWithOptions', () => {
218223
beforeEach(() => {
219224
vi.clearAllMocks()
220225
mockResolvePiSandboxLifetime.mockReturnValue(undefined)
226+
mockSleepUntilAborted.mockResolvedValue(undefined)
221227
mockFetchDiagnostics.mockResolvedValue(new Map([['check:ci', 'failure output']]))
222228
mockReplyAndResolve.mockResolvedValue({
223229
repliesPosted: 1,
@@ -507,11 +513,13 @@ describe('runBabysitPiWithOptions', () => {
507513
const result = await runBabysitPiWithOptions(
508514
params({ maxRounds: 1 }),
509515
{ onEvent: vi.fn() },
510-
{ roundWaitMs: 0 }
516+
{ roundWaitMs: 1 }
511517
)
512518

513519
expect(result).toMatchObject({ stopReason: 'clean', rounds: 0, checksGreen: true })
514520
expect(runCalls.some(({ command }) => command.includes('pi -p --mode json'))).toBe(false)
521+
expect(mockSleepUntilAborted).toHaveBeenCalledWith(1, expect.any(AbortSignal))
522+
expect(runCalls.some(({ command }) => command === 'true')).toBe(true)
515523
})
516524

517525
it('returns pushed_awaiting_confirmation after replying against a lagging GitHub record', async () => {

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const MAX_REVIEW_PROMPT_BYTES = 250_000
8989
const MAX_CHECK_PROMPT_BYTES = 400_000
9090
const MIN_ROUND_BUDGET_MS = 5 * 60 * 1000
9191
const ROUND_FINALIZATION_RESERVE_MS = 2 * FINALIZE_TIMEOUT_MS
92+
const SANDBOX_KEEPALIVE_INTERVAL_MS = 4 * 60 * 1000
9293

9394
const BABYSIT_GUIDANCE =
9495
'You are fixing an existing pull request in a long-lived automated sandbox. Make only minimal ' +
@@ -487,6 +488,25 @@ async function waitForHeadConvergence(
487488
return 'lagging'
488489
}
489490

491+
async function waitWithSandboxKeepalive(
492+
runner: PiSandboxRunner,
493+
durationMs: number,
494+
signal: AbortSignal
495+
): Promise<void> {
496+
let remainingMs = durationMs
497+
while (remainingMs > 0) {
498+
const intervalMs = Math.min(remainingMs, SANDBOX_KEEPALIVE_INTERVAL_MS)
499+
await sleepUntilAborted(intervalMs, signal)
500+
if (signal.aborted) throw new Error('Pi run aborted')
501+
const keepalive = await raceAbort(
502+
runner.run('true', { timeoutMs: FINALIZE_TIMEOUT_MS }),
503+
signal
504+
)
505+
if (keepalive.exitCode !== 0) throw new Error('Babysit sandbox keepalive failed')
506+
remainingMs -= intervalMs
507+
}
508+
}
509+
490510
function outstandingReason(
491511
fallback: BabysitStopReason,
492512
threads: BabysitThreadsState,
@@ -664,8 +684,7 @@ export async function runBabysitPiWithOptions(
664684
)
665685
return resultFor(totals, reason, progress, threadsClean, latestChecks!.checksGreen)
666686
}
667-
await sleepUntilAborted(options.roundWaitMs, signal)
668-
if (signal.aborted) throw new Error('Pi run aborted')
687+
await waitWithSandboxKeepalive(runner, options.roundWaitMs, signal)
669688
snapshot = await fetchBabysitSnapshot(params, signal)
670689
assertBabysitPinned(
671690
{ headSha: pinnedHeadSha, headRef: pinnedHeadRef, baseRef: pinnedBaseRef },
@@ -957,8 +976,7 @@ export async function runBabysitPiWithOptions(
957976
remainingBeforeWait >
958977
options.roundWaitMs + MIN_ROUND_BUDGET_MS + ROUND_FINALIZATION_RESERVE_MS
959978
) {
960-
await sleepUntilAborted(options.roundWaitMs, signal)
961-
if (signal.aborted) throw new Error('Pi run aborted')
979+
await waitWithSandboxKeepalive(runner, options.roundWaitMs, signal)
962980
}
963981

964982
snapshot = await fetchBabysitSnapshot(params, signal)

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,16 @@ describe('Babysit GitHub orchestration', () => {
161161
})
162162
})
163163

164+
it('accepts a complete empty rollup when the commit has no checks', async () => {
165+
mockExecuteTool.mockResolvedValueOnce(checkPage([], { state: null }))
166+
167+
const state = await fetchBabysitCheckState(params, HEAD_SHA)
168+
169+
expect(state.checks).toEqual([])
170+
expect(state.checksGreen).toBe(true)
171+
expect(state.contextRequirements).toEqual(new Map())
172+
})
173+
164174
it('treats EXPECTED and incomplete checks as pending and optional failures as non-blocking', async () => {
165175
mockExecuteTool.mockResolvedValueOnce(
166176
checkPage([

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -433,14 +433,10 @@ export async function fetchBabysitCheckState(
433433
cursor = endCursor
434434
if (page === MAX_PAGES - 1) throw new Error('Check listing exceeded its page bound')
435435
}
436-
const canSynthesizeMissingContexts = !!initialRequirements?.size
437-
if (expectedTotal === undefined || (expectedTotal === 0 && !canSynthesizeMissingContexts)) {
436+
if (expectedTotal === undefined) {
438437
throw new Error('GitHub returned no check rollup for the pinned commit')
439438
}
440-
if (
441-
(rollupState === null || rollupState === undefined) &&
442-
!(expectedTotal === 0 && canSynthesizeMissingContexts)
443-
) {
439+
if ((rollupState === null || rollupState === undefined) && expectedTotal !== 0) {
444440
throw new Error('GitHub returned checks without a rollup state')
445441
}
446442
} catch (error) {

0 commit comments

Comments
 (0)