Skip to content

Commit 49c1349

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(pi): classify babysit finalize failures
1 parent 5c23136 commit 49c1349

2 files changed

Lines changed: 61 additions & 16 deletions

File tree

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,30 @@ describe('runBabysitPiWithOptions', () => {
519519
})
520520
})
521521

522+
it('reports missing finalize protocol markers as an agent failure', async () => {
523+
mockFetchSnapshot.mockResolvedValue(snapshot)
524+
mockFetchThreads.mockResolvedValue({
525+
actionable: [trustedThread],
526+
skipped: [],
527+
totalUnresolved: 1,
528+
latestReview: null,
529+
})
530+
mockFetchChecks.mockResolvedValue(greenChecks)
531+
const { runner, runCalls } = makeRunner({
532+
prepareStdout: '__NEEDS_PUSH__=1\n',
533+
})
534+
mockWithPiSandbox.mockImplementation(async (callback) => callback(runner))
535+
536+
const result = await runBabysitPiWithOptions(params(), { onEvent: vi.fn() })
537+
538+
expect(result).toMatchObject({
539+
stopReason: 'agent_failure',
540+
rounds: 1,
541+
commitsPushed: 0,
542+
})
543+
expect(runCalls.some(({ command }) => command.includes('CURRENT_DIGEST='))).toBe(false)
544+
})
545+
522546
it('stops on head movement at the pre-push phase boundary', async () => {
523547
mockFetchSnapshot
524548
.mockResolvedValueOnce(snapshot)

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

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
GIT_CONFIG_DIGEST_LINE,
5050
GIT_CONFIG_DIGEST_MARKER,
5151
MAX_DIFF_BYTES,
52+
MIN_PI_TIMEOUT_MS,
5253
PI_TIMEOUT_MS,
5354
PROMPT_PATH,
5455
PUSH_ERROR_MAX,
@@ -87,7 +88,7 @@ const MAX_CHANGED_FILES = 50
8788
const MAX_FAILING_CHECKS_IN_PROMPT = 20
8889
const MAX_REVIEW_PROMPT_BYTES = 250_000
8990
const MAX_CHECK_PROMPT_BYTES = 400_000
90-
const MIN_ROUND_BUDGET_MS = 5 * 60 * 1000
91+
const MIN_ROUND_BUDGET_MS = MIN_PI_TIMEOUT_MS
9192
const ROUND_FINALIZATION_RESERVE_MS = 2 * FINALIZE_TIMEOUT_MS
9293
const SANDBOX_KEEPALIVE_INTERVAL_MS = 4 * 60 * 1000
9394

@@ -175,6 +176,16 @@ interface RoundFinalize {
175176
diff: string
176177
}
177178

179+
class BabysitFinalizeError extends Error {
180+
constructor(
181+
public readonly reason: BabysitStopReason,
182+
message: string
183+
) {
184+
super(message)
185+
this.name = 'BabysitFinalizeError'
186+
}
187+
}
188+
178189
function untrustedJson(value: unknown): string {
179190
return JSON.stringify(value).replace(/[<>&]/g, (character) => {
180191
if (character === '<') return '\\u003c'
@@ -416,14 +427,18 @@ async function finalizeRound(
416427
signal
417428
)
418429
if (prepare.exitCode !== 0) {
419-
throw new Error(
430+
throw new BabysitFinalizeError(
431+
'agent_failure',
420432
`Babysit finalize refused repository state: ${truncate(prepare.stderr || prepare.stdout, PUSH_ERROR_MAX)}`
421433
)
422434
}
423435
const noChanges = prepare.stdout.includes('__NO_CHANGES__=1')
424436
const needsPush = prepare.stdout.includes('__NEEDS_PUSH__=1')
425437
if (noChanges === needsPush)
426-
throw new Error('Babysit finalize returned inconsistent change state')
438+
throw new BabysitFinalizeError(
439+
'agent_failure',
440+
'Babysit finalize returned inconsistent change state'
441+
)
427442
if (noChanges) {
428443
return { commitPushed: false, newSha: roundBaseSha, changedFiles: [], diff: '' }
429444
}
@@ -435,15 +450,25 @@ async function finalizeRound(
435450
const changedFiles = extractMarkerValues(prepare.stdout, '__CHANGED__=')
436451
const newSha = extractMarkerValues(prepare.stdout, '__NEW_SHA__=')[0]
437452
if (!newSha || !Number.isSafeInteger(cumulativeDiffBytes)) {
438-
throw new Error('Babysit finalize omitted its commit or diff bounds')
453+
throw new BabysitFinalizeError(
454+
'agent_failure',
455+
'Babysit finalize omitted its commit or diff bounds'
456+
)
439457
}
440458
if (cumulativeChangedFiles.length > MAX_CHANGED_FILES || cumulativeDiffBytes > MAX_DIFF_BYTES) {
441-
throw new Error('Babysit cumulative change bounds were exceeded')
459+
throw new BabysitFinalizeError(
460+
'bounds_exceeded',
461+
'Babysit cumulative change bounds were exceeded'
462+
)
442463
}
443464
if (cumulativeChangedFiles.some((file) => file === '.github' || file.startsWith('.github/'))) {
444-
throw new Error('Babysit refuses to push changes under .github/')
465+
throw new BabysitFinalizeError(
466+
'refused_content',
467+
'Babysit refuses to push changes under .github/'
468+
)
445469
}
446470

471+
const diff = scrubPiSecrets(await runner.readFile(DIFF_PATH), secrets)
447472
assertBabysitPinned(snapshot, await fetchBabysitSnapshot(params, signal))
448473
const push = await raceAbort(
449474
runner.run(BABYSIT_PUSH_SCRIPT, {
@@ -462,14 +487,14 @@ async function finalizeRound(
462487
signal
463488
)
464489
if (!push.stdout.includes('__PUSHED__=1')) {
465-
throw new Error(
490+
throw new BabysitFinalizeError(
491+
'push_rejected',
466492
`git push failed: ${truncate(
467493
scrubGitSecrets(push.stderr || push.stdout || 'unknown error', params.githubToken),
468494
PUSH_ERROR_MAX
469495
)}`
470496
)
471497
}
472-
const diff = scrubPiSecrets(await runner.readFile(DIFF_PATH), secrets)
473498
return { commitPushed: true, newSha, changedFiles, diff }
474499
}
475500

@@ -567,7 +592,7 @@ export async function runBabysitPiWithOptions(
567592
if (lifetime < MIN_ROUND_BUDGET_MS) {
568593
cancellation.cleanup()
569594
throw new Error(
570-
'Babysit needs at least five minutes of runtime; use an async trigger and a longer Pi sandbox lifetime.'
595+
'Babysit needs at least one minute of runtime; use an async trigger and a longer Pi sandbox lifetime.'
571596
)
572597
}
573598

@@ -813,13 +838,9 @@ export async function runBabysitPiWithOptions(
813838
const reason: BabysitStopReason =
814839
error instanceof BabysitGitHubError
815840
? error.reason
816-
: message.includes('.github/')
817-
? 'refused_content'
818-
: message.includes('bounds')
819-
? 'bounds_exceeded'
820-
: message.includes('push failed')
821-
? 'push_rejected'
822-
: 'refused_content'
841+
: error instanceof BabysitFinalizeError
842+
? error.reason
843+
: 'agent_failure'
823844
return resultFor(totals, reason, progress, threadsClean, latestChecks!.checksGreen)
824845
}
825846

0 commit comments

Comments
 (0)