Blolabel
← Notes
analysis 8 min read

Four of six failures in our first trusted run were our own bugs, not the model's

The score said 85.4% of attempted tasks passed. Reading every failure against the agent's own transcript said something more useful: the instrument was wrong in three places, and no number of reruns would have found it.

The first run of this benchmark that we trusted scored 85.4% of attempted tasks. Then we read every failure against what the agent actually said and did on the phone. Of six failures, three were caused by our own checks, one was a task that could only be failed because of the owner's device, and one or two were the model's. The audit changed the interpretation of the run completely, and it took less time than the run did.

Abstract technical illustration of a measuring instrument whose own scale is bent, so the reading it produces is a property of the instrument rather than the sample

What the audit found, task by task

The method is the cheap part: for each failed task, put the agent's own transcript next to the device-state check that failed it, and ask which of the two is wrong. The agent's account is not evidence that it succeeded, because a model that believes it tapped Send will say it sent the message. It is excellent evidence about the harness, because a model that says "I created the reminder and the list went from 5 items to 6" and then fails a check is telling you the check is looking in the wrong place.

taskwhat the check saidwhat the transcript showedverdictsource
contacts.searchno "Cancel" button foundthe agent completed the search and listed the resultsour bug: that UI has no CancelFINDINGS s.12
multi.settings_to_safaritask abandoned mid-waythe agent read the iOS version correctly, then stoppedour bug: shared mode paused it, believing the owner had picked the phone upFINDINGS s.12
reminders.createthe reminder text was not on screenthe agent created it and verified the list count going 5 to 6our bug: the check refused to scrollFINDINGS s.12
safari.newtabno new tabthe agent diagnosed 502 open tabs and that iOS disables the control at roughly 500our unfair task, retired rather than fixedFINDINGS s.12
safari.navigate.exampleforeground is springboard"Safari is open and showing example.com"genuine catch, and the reason the checks existFINDINGS s.12
clock.timer.setno timer set26 turns, 228 seconds, no resultgenuine model failure: picker wheels are hardFINDINGS s.12, s.15
system.notifications.openno "Clear" buttonNotification Centre was open and emptyour bug, found later: Clear exists only when notifications are waitingFINDINGS s.17

The last row is the one that matters most, because it was found after the first three were fixed. It is the fourth check of the same shape: an assertion on a control that only exists in one of the device's two legitimate states. On an empty phone the task is unpassable no matter what the agent does, and nothing in the result file says so.

Where six failures actually came from One run, 32 tasks, every failure read against the agent's own transcript 3 our check was wrong 1 unfair task, retired 2 the model's 0 2 3 failures Four of six belonged to the benchmark. The run's headline number was the same either way.
The attribution is the result. A benchmark that is not read this way publishes its own defects as somebody else's incapability, and the score looks respectable in both cases.

Five ways this rig produced a number that was not true

Four of them made the number look better. That direction is not a coincidence: a harness fault usually removes work from the model or credits work it did not finish, and only rarely invents extra difficulty. Each one is now a guard in code rather than a note in a document.

faultwhat would have been publishedthe guard nowwhere
Rate limit after 5 of 32 tasks, the other 27 recorded as model failures12.5%UsageLimitReached, the task is marked skipped and the run aborts with an explanationbench/runner.py
The phone locked mid-run, every task after it failed identicallya page of red that reads as a model that cannot use a phoneRigUnavailable, raised on a locked phone that does not come backbench/runner.py
A verification step created a WebDriverAgent session, which sent the phone home before reading itevery task failing with "foreground is springboard"the observation path uses only sessionless routesFINDINGS s.6
clock.alarm.set_time hit its 240 second budget with zero completed turns and $0.00 spent, then passed both checks from what was left on screena passresult.passed = passed and not timed_out, and checks match the wheel's own value rather than any digits on screenbench/runner.py, FINDINGS s.21
Retired tasks kept their last result in the file and kept being counted; a leaderboard over 74 tasks sat above a footer over 60two different rates on one pagescoring keeps only live tasks and only the latest attempt per task, with a publication mode that scores over published tasksbench/runner.py score()

The pattern behind all five is one distinction: the model tried and got it wrong and we could not ask the model are different facts. A benchmark that cannot tell them apart is not producing a noisy number, it is producing fiction, and the fiction is stable across reruns because the cause is in the harness.

Four places an environment problem becomes a capability result task starts setup, then ask agent runs turns and budget checks read device state, not the report aggregate score() over live tasks rate limit, locked phone 240 s budget, max turns check asserts on a control the device may not draw retired tasks still counted NOT SCORED excluded from the rate, counted and reported FAILED the model was asked, had the budget, and did not do it The default matters. Before the guards, every one of these arrows pointed at FAILED, and nothing in the results file recorded that the model had never been asked. phoneshell/bench/runner.py: UsageLimitReached, RigUnavailable, timed_out, score() over live tasks only
Every arrow that used to point at FAILED made the model look worse, except the timeout, which made it look better. Both directions are errors of the same kind.

This is not a local problem

The Agentic Benchmark Checklist (arXiv:2507.02825, first posted 3 July 2025, v5 on 7 August 2025) reports the same class of defect in published benchmarks: SWE-bench Verified uses insufficient test cases, TAU-bench counts empty responses as successful, and the authors put the resulting misestimation at up to 100% in relative terms. Applying the checklist to CVE-Bench reduced its performance overestimation by 33%. TAU-bench counting an empty response as a success is the same fault as our timed-out alarm task passing its checks with zero turns and no spend: a grader that reads the environment without asking whether the agent was ever in a position to change it.

The method we used by hand has since been automated. "Automated Transcript Analysis for Detecting Flaws in Agentic Benchmarks" (arXiv:2607.27518, 29 July 2026) runs scanners over benchmark transcripts looking for ground truth access, tool failure, guessing vulnerability and answer format ambiguity, and reports verified quality issues in five widely used benchmarks. Our four categories were different only because our environment is a phone: a check asserting on a control iOS does not draw, a courtesy feature pausing a measured run, a check that would not scroll, and a task whose outcome depended on how many Safari tabs the owner happened to have open.

What to do with this if you run an agent benchmark

Read the transcripts of your failures before you publish the number. Not a sample: all of them, at least once, at the point where the suite is small enough that this is possible. It is the cheapest quality control available and it is the only one that catches a check that is wrong in the same direction every time.

Then make the two failure modes structurally different in your results file. A boolean passed cannot express "we could not ask the model", and if the only place that fact lives is an exception message, the aggregate will silently absorb it. Our row carries skipped and skip_reason alongside passed, and the published rate is computed over scored tasks with the skip count printed next to it.

Three rules came out of the audit and each generalises beyond phones. "Not visible right now" and "not there" are different facts, so a check that refuses to scroll blames the model for the harness's impatience. Assert on the surface, not on a specific button, because button sets change between OS versions and an assertion on one makes the benchmark rot. And a measured run must take the device: a feature that politely yields to a human who is not there is a scoring bug during measurement.

A judgement, marked as one: the audit is worth more than the run. The score told us roughly what we already believed. The audit found four defects in the instrument, and three of them would have survived any number of reruns.

Check it yourself

The guards are all in one file, and the results are line-delimited JSON, so the arithmetic is inspectable without running anything on a phone.

git clone https://github.com/Blomega/phoneshell && cd phoneshell

# the three guards, in order of the diagram above
grep -n "class UsageLimitReached" -A 8 phoneshell/bench/runner.py
grep -n "class RigUnavailable" -A 8 phoneshell/bench/runner.py
grep -n "result.passed = passed and not timed_out" -B 6 phoneshell/bench/runner.py

# scoring excludes skipped tasks and retired tasks, and keeps the latest attempt only
grep -n "def score" -A 30 phoneshell/bench/runner.py

# on your own results file: how many rows were never scored, and why
jq -r 'select(.skipped==true) | [.task_id,.model,.skip_reason] | @tsv' runtime/bench/latest.jsonl

# the audit itself: every failure next to what the agent said about it
jq -r 'select(.passed==false and .skipped!=true) | [.task_id,.turns,.seconds,.agent_said] | @tsv' \
  runtime/bench/latest.jsonl

The last command is the whole method. If the fourth column explains the task and the check still failed it, the defect is yours.

What would prove this wrong

Sources

  1. phoneshell, FINDINGS.md sections 7, 12, 17 and 21, the audit and the five fabricated numbers, published 16 September 2026. https://github.com/Blomega/phoneshell/blob/main/FINDINGS.md
  2. phoneshell, phoneshell/bench/runner.py: UsageLimitReached, RigUnavailable, the timeout rule and score() over live tasks only. https://github.com/Blomega/phoneshell/blob/main/phoneshell/bench/runner.py
  3. Zhu et al., Establishing Best Practices for Building Rigorous Agentic Benchmarks, arXiv:2507.02825, 3 July 2025, v5 on 7 August 2025. SWE-bench Verified's insufficient tests, TAU-bench counting empty responses as successful, misestimation up to 100% in relative terms. https://arxiv.org/abs/2507.02825
  4. Mohl et al., Automated Transcript Analysis for Detecting Flaws in Agentic Benchmarks, arXiv:2607.27518, 29 July 2026. https://arxiv.org/abs/2607.27518
  5. Blolabel, phoneshell-bench, the published task set and results. https://huggingface.co/datasets/blolabel/phoneshell-bench