Game Summary on device: measure both runtimes, and fix what was being measured as model quality - #138
Merged
Conversation
Game Summary is the surface where an on-device model is still attached (iOS), and the only one with no response validator at all: whatever the model writes reaches the user. The Move Coach verdict does not transfer to it automatically — a summary is a different task with a different budget, behind a button rather than in an automatic panel — so it gets measured on its own terms. The bench could not do that. It carried one hand-written game whose MoveRecords were fabricated: `uci = "e2e4"` on every ply, the start position as every `fenAfter`, and centipawn losses chosen by hand. Since the summary's whole job is to pick the moments that decided *this* game, a fixture whose turning points were chosen by hand cannot tell you whether it did. `tools/generate_summary_fixtures.py` plays real games with the player's side deliberately weakened and assesses every player ply with Stockfish. The fixture carries raw engine numbers only — `MoveAssessor` derives MoveClass and winPercentLost in `SummaryFixtures`, so the bench cannot drift from the app's own definition of a blunder. Each row records the deterministic composer's text beside the model's, so the two are scored from one file, as the coach's are. `IosSummaryBench` is new: Game Summary had no iOS bench at all, which is why the one platform still running a model on it had never been measured there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 real assessed games, same fixtures on both platforms. AICore succeeds on 7 of 12 at ~12 s and cites exactly the code-chosen turning points on 6 of those 7. Foundation Models succeeds on 4 of 12 — it refuses two games in three with "An unsupported language or locale was used", chess notation evidently not being natural language enough for it — and every summary it does produce stops mid-sentence. That reverses the Move Coach result, where Foundation Models was the faster and more fluent writer. On summaries it is also the less reliable one, and a wrong [move-N] on the engine's preferred move navigates the user to the wrong ply, since B16 turns those tags into board jumps. Recommendation recorded: leave Android's Game Summary deterministic. Not because the model writes badly — this is the best on-device output measured on the project — but because 58% success at 12 s loses to a template that cites 3 of 3 turning points instantly and cannot fail. The blockers are truncation and the latency budget, and neither is a model problem. Also records a polish bug in the deterministic text itself: it emits "This was a inaccuracy" and "This was a good". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first Game Summary pass reported 7/12 for AICore and 4/12 for Foundation
Models, and concluded that Foundation Models refuses two games in three and
truncates every success. All three failure modes were ours:
* AICore refuses to generate unless the app is in the foreground. The keyguard,
not the screen timeout, is what stops the bench activity being resumed, so
`svc power stayon usb` did not prevent it — it cost 3 rows of the first run
and all 12 of the second, and the two rows first reported as "exceeded
latency budget" at ~46 s were the cold start right after the blocked ones.
* `noRepeatNgramSize = 4` is tuned for the coach's single sentence. A summary
of three turning points is a parallel list, so the guard cut real answers at
the start of the second "so this was another small inaccuracy" — 4 of 4
Foundation Models successes and 1 of 7 AICore ones.
* The raw PGN in the prompt tripped Apple's language guardrail (8 of 12
prompts rejected in 15-20 ms, the same 8 across two runs) and was the sole
source of Android's invented narrative — "contributed to the loss" on a game
that has no result. The turning points already carry every fact the summary
may state.
Both runtimes now score 12/12 on the same fixtures. AICore cites exactly the
code-chosen turning points in 11 of 12 at ~12 s; Foundation Models is eight
times faster and factually right but writes as the player ("I made two
significant mistakes") and drops the [move-N] citations B16 turns into board
jumps in 9 of 12.
Also fixes the deterministic floor this all falls back to: "This was a
inaccuracy." / "This was a good." shipped on both platforms, and a MISTAKE could
be followed by "This move was slightly inaccurate" one sentence later, because
the class comes from cpLoss and the intuition from a win-percent delta.
MainActivity now holds the whole ML Kit wiring behind one ATTACH_ON_DEVICE_AI
constant, which is false. The probe was previously unreachable outside bench/;
the constant sits inside the probe call because probeAvailableLocalVendors()
awaits an AICore feature fetch on a DOWNLOADABLE device, so inactive has to mean
no network. Modelled on googlesamples/mlkit android/genai, which is also where
the Candidate.finishReason == MAX_TOKENS signal we were dropping comes from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ATTACH_ON_DEVICE_AI attached the Move Coach and Game Summary together, and the two decisions have gone opposite ways: the coach is settled against on measurement, while the summary is 12/12 and waiting on a validator. One constant meant flipping the open one would drag the settled one with it. Also moves enableDeterministic() above the probe and makes it unconditional. It used to run only on the not-attached path, so with a model attached the summary button was missing until the probe returned — and that probe awaits an AICore feature download on a DOWNLOADABLE device. The deterministic summary now stands from the first frame and the orchestrator takes over when it arrives; deterministicEnabled only applies while the orchestrator is null, so the two cannot fight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 12-game set is small for a surface where one bad summary is the whole answer. Regenerated at 50 and re-ran both platforms. Android holds: 43/43 (7 rows lost to one contiguous ErrorCode 30 block, which is the harness), 86% citing exactly the code-chosen plies, zero invented tags, zero truncation, p95 13.2 s. iOS gets worse under a larger sample, and in the way that matters most: 2 of 50 answers invent a [move-N]. At 12 games the citation problem looked like omission; at 50 it is fabrication, and a fabricated tag is the one defect the user cannot detect because B16 turns it into a board jump to a ply that was never a turning point. Half the answers still carry no citation at all and half are written in first person as the player. The plan's acceptance criteria are rewritten against these numbers rather than against my guess: zero invented tags is a hard gate, not a percentage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It was the thinnest of the three: no file paths, no JSONL columns, no decision rule. Rules Q&A is the only surface with a model live in production on both phones and the only one never measured, so the plan should not leave the next agent to invent the shape. Adds the fixture design (one case per passage phrased as a player would ask, the draw-dead-position/draw-agreement near-miss pair, and out-of-corpus questions where the honest answer is that the corpus does not cover it), the row schema with the grounded answer as the column to beat, what to score, and the decision the run exists to make: whether the model turn earns its place at all, given the retrieval floor already answers correctly with a citation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Game Summary had no response validator at all — any non-blank text reached the
user — and it is live on iOS today, where half the answers carry no citation, a
quarter are written in first person as the player, and two of fifty fabricate a
[move-N] that B16 turns into a board jump to a ply that was never a turning
point. Five rules now gate it: citation set, coverage, move attribution, voice,
piece type. A rejection lands on GameSummaryGrounding, which is what ships
today, so a false rejection costs the wait and nothing else.
Two further rules were built, measured, and removed. Both passed their own unit
tests and produced zero true positives against 43 real summaries:
* Class fidelity could not see negation, so it rejected "while these aren't
huge blunders" and "[move-17] with Qc2 wasn't a blunder"; and its per-tag
segmentation ran to the next [move-N], so the last citation swallowed the
closing paragraph and matched "focusing on those moments of inaccuracy".
* The best-move-described-as-played branch required the engine's move to
appear after "instead of", so "opting for e4 instead of Nd2 would have been
a stronger choice" — correct English, correct chess — was rejected on six
summaries.
Together they accepted only 58% of the AICore output the benchmark had already
judged good, which would have meant one summary in two costing a 12 s wait and
then showing the composed text anyway. The invented-move check is also narrowed
to unambiguous move tokens, because the bare-pawn form cannot be told apart from
prose naming a square and rejected "Sacrificing the Bishop on g6".
GameSummaryValidatorFieldTest is the check that found all of this and the reason
it cannot recur: it replays 43 AICore and 20 Foundation Models summaries from the
50-game run, and fails the build if acceptance drops below 85% or if any rule
other than incomplete coverage ever fires on an AICore summary.
Also syncs ScorecardWriter with the scorecard.md note edited by hand in #137, so
the next :evals:run stops reverting it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ber4444
marked this pull request as ready for review
August 15, 2026 15:59
ber4444
pushed a commit
that referenced
this pull request
Aug 16, 2026
Three of its statements were false on main, two of them self-contradictions inside the same file: the header said the work was on an unmerged branch, and "Where things stand" said Game Summary has no response validator at all — in a file whose Task 1 is marked DONE and describes shipping one. A fresh agent reading top to bottom hit the false version first. The scorecard leftover is struck through as done here. The DeviceRunScorer one is NOT: this PR fixed the failure mode, not the gap. Sharpened it to name the blocker (a summary row carries no per-ply assessments, so the validated request cannot be rebuilt), where the fix belongs (the bench runners, not the scorer), and the wrong fix to avoid — scoring the two rules that survive the missing facts would publish a number that looks like the coach column and is not one. Dropped the "branch is unmerged" leftover, which #138 resolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ber4444
added a commit
that referenced
this pull request
Aug 16, 2026
) * Publish the measured device rows, and refuse a summary run by name Two items #138 left open, both of which mislead a reader rather than break a build. The scorecard's `aicore-nano-fast` row read 0.0% grounded / 100% length-rejected at n=10, and `foundation-models-ios` read n=1. Between them they measured three of our own bugs — a client config asking for a model variant the device does not provision, a terminal `Final` event that appended the answer to itself, and a bench runner that supplied no facts — plus a length rule that rejected instead of trimming. Their notes said "re-measure before trusting this row". #137 re-measured; this publishes it. Replaced with the three columns of the 2026-08-15 hundred-case run, including the deterministic baseline the other two are scored against, which the scorecard has never carried. Its 28% grounding-violation figure ships with the reason it overstates: 17 of the 28 are the echoed-prompt rule firing on the column against itself, because that line *is* the prompt's baseline sentence. Superseded rows are deleted rather than left under a warning — both articles quote these numbers, and a reader who follows the repo link must not find 0.0% grounded next to a published 91/100. `scoreDeviceRun` pointed at a Game Summary JSONL died with MissingFieldException and a 40-line stack trace, which reads like a broken scorer rather than the wrong input file — and the two files sit in the same directory under similar names. It now refuses by name and says why: the summary validator derives turning points from the per-ply assessments, and a summary row carries pgn/plies/playerBlunders and no assessments, so the request it validated against cannot be rebuilt. It deliberately does not score the subset of rules that survive that gap; a partial number that looks like the coach column and means something else is the "second scorer next to the data" this file exists to prevent. Full summary scoring is a bench-schema change, left to its own PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Update the next-steps doc for what #138 and this PR actually changed Three of its statements were false on main, two of them self-contradictions inside the same file: the header said the work was on an unmerged branch, and "Where things stand" said Game Summary has no response validator at all — in a file whose Task 1 is marked DONE and describes shipping one. A fresh agent reading top to bottom hit the false version first. The scorecard leftover is struck through as done here. The DeviceRunScorer one is NOT: this PR fixed the failure mode, not the gap. Sharpened it to name the blocker (a summary row carries no per-ply assessments, so the validated request cannot be rebuilt), where the fix belongs (the bench runners, not the scorer), and the wrong fix to avoid — scoring the two rules that survive the missing facts would publish a number that looks like the coach column and is not one. Dropped the "branch is unmerged" leftover, which #138 resolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Bot <bot@example.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Measures Game Summary on both on-device runtimes, fixes the three bugs that first-pass measurement mistook for model quality, adds the response validator this surface never had, and leaves the attach switched off behind a per-surface constant.
The finding: three bugs were measured before either model was
The first pass reported 7/12 for AICore and 4/12 for Foundation Models, and concluded that Foundation Models "refuses two games in three and truncates every success". All three failure modes were ours.
[ErrorCode 30] Background usage is blocked— AICore refuses to generate unless the app is foregrounded.adb shell am starton a locked device produces a full JSONL of sub-second fallbacks that measure nothing: 3 rows of the first run, all 12 of the second.svc power stayon usbdoes not prevent it, because the keyguard rather than the screen timeout is what stops the activity being resumed. The two rows first reported as "inference exceeded latency budget" at ~46 s were the cold start immediately after the blocked ones.noRepeatNgramSize = 4— B15's guard, tuned for the coach's single sentence. A summary of three turning points is a parallel list, and a parallel list repeats its connectives by construction, so it cut real answers at the start of the second "so this was another small inaccuracy". 4 of 4 Foundation Models successes, 1 of 7 AICore.Removing the PGN took Foundation Models from 4/12 to 12/12 and Android's unsourced-narrative rows from 6/12 to 0/12.
The numbers, at 50 games
[move-N]† 7 of 50 rows lost to one contiguous
ErrorCode 30block despite the foreground fix. A contiguous block of sub-second fallbacks is the harness every time.The two invented tags are the finding. At 12 games iOS had none and the citation gap looked like omission; at 50 it is fabrication, and a fabricated
[move-N]is the one defect a user cannot detect — B16 turns it into a board jump to a ply that was never a turning point. Android fabricated none in 43. At 50 games the iOS tail is also worse than the small sample suggested: some rows are not summaries at all, but refusals ("I'm sorry, but I cannot summarize the mistakes in the game") and apologies in character ("I apologize for the mistakes I made… I will try to improve").What changed in the product
noRepeatNgramSizeis 8 on this surface, andtrimIncompleteSummaryTaildrops a ragged tail if anything else ever cuts one.GameSummaryResponseValidator— the surface had none, and it is live on iOS today. Five rules: citation set, coverage, move attribution, voice, piece type. A rejection lands onGameSummaryGrounding, which is what ships today, so a false rejection costs the wait and nothing else. Measured: accepts 37/43 AICore, rejects 42/50 Foundation Models."This was a inaccuracy."and"This was a good."on both platforms, and a MISTAKE could be followed by "This move was slightly inaccurate" one sentence later, because the class comes fromcpLossand the intuition from a win-percent delta.MainActivity.attachOnDeviceAi()holds probe → executor → both orchestrators behindATTACH_MOVE_COACHandATTACH_GAME_SUMMARY, bothfalse. The probe previously had no caller outsidebench/. Three details are load-bearing and documented at the call site: the constant sits inside the probe becauseprobeAvailableLocalVendors()awaits an AICore feature fetch on aDOWNLOADABLEdevice;isAppForegroundedis read per request so a backgrounded request falls back instead of erroring; and the vendor list is probed once and reused. Modelled on googlesamples/mlkit android/genai, which is also where theCandidate.finishReason == MAX_TOKENSsignal we were dropping comes from.keepBenchInForeground()(bench-only) holdsFLAG_KEEP_SCREEN_ON+setShowWhenLocked.ScorecardWriternow matches thescorecard.mdnote hand-edited in ML Kit on Android: availability was a client-config bug, and the "repetition loop" was ours #137, so the next:evals:runstops reverting it.Two validator rules were built, measured, and removed
Both passed their own unit tests and produced zero true positives against 43 real summaries:
[move-N], so the last citation's segment swallowed the closing paragraph and matched "focusing on those moments of inaccuracy".Together they accepted only 58% of output the benchmark had already judged good, which would have meant one summary in two costing a 12 s wait and then showing the composed text anyway.
GameSummaryValidatorFieldTestis the check that found this and the reason it cannot recur: it replays 43 AICore and 20 Foundation Models summaries from the 50-game run, with the turning points the device computed, and fails the build if acceptance drops below 85% or if any rule other than incomplete coverage fires on an AICore summary.Recommendation
Do not flip either constant in this PR. Android is now a genuine trade rather than a loss — 12 s and a coach's voice against instant and robotic, citations intact either way. The remaining question is not quality but whether one summary in seven costing an 11 s wait for the composed text is a good deal; the validator makes that outcome safe, not free.
iOS is not ready, and it already ships: first person in half the answers, no citation in half, and two fabricated board jumps. The validator now catches all of it, which is the argument for merging this regardless of any attach decision. The fix for iOS is the prompt, not the validator — see the plan.
Next steps
docs/plans/on-device-ai-next-steps.md:Write— done in this PR, with the two removed rules recorded.GameSummaryResponseValidatorLeftovers, not addressed here
DeviceRunScorerhas no Game Summary mode — summary runs are scored ad hoc, the "second scorer written next to the data" problem:evalsexists to prevent.evals/scorecard.md'saicore-nano-fastrow is stale (0.0% grounded) — it measured three separate bugs plus a preference thatFEATURE_NOT_FOUNDs on a Pixel 10.run-aspush.Testing
All CI green on
06c01aa9: Android emulator suite, iOS simulator + Swift tests, the grounding regression gate, and CodeQL.The field test runs in CI through
:ondeviceai:checkin the Android job — worth knowing, because the apple job's task list does not include:ondeviceai:desktopTest, socheckis the only thing that executes it.Locally:
:ondeviceai:check(all targets),:evals:test,:evals:run(regeneratesscorecard.mdwith no drift) and:androidApp:assembleDebug.Benchmarks ran on a Pixel 10 Pro XL and an iPhone 17 Pro simulator (iOS 26.5); raw JSONL stays in
build/bench/per the existing convention, with repro commands in the benchmark doc. The 63 rows the validator is tested against are checked in atonDeviceAi/src/desktopTest/resources/game-summary-field-corpus.jsonl.🤖 Generated with Claude Code