Repository navigation
Description
A fill on the penalized coordinate route committed a value that is neither the requested text nor a prefix of it, and stayed that way. The commit wait caught it — #1995 working as designed — but it turns an expensive lane red on unrelated PRs, which is the #1874 symptom from a third mechanism.
The trace
From the iOS smoke lane of PR #2059 (run 33067586234), a PR that changes one workflow file, one test and vitest.config.ts — nothing reachable from the runner.
Step: agent-device fill id="field-email" ada@example (11 characters).
AGENT_DEVICE_RUNNER_TEXT_ENTRY_ROUTE route=synthesized-first-responder-replacement
[DEBUG-1874] synthesize posted 11 chars status=0 tookMs=1084
[DEBUG-1874] wait start expectedLen=11 route=replacement
[DEBUG-1874] poll t=1102ms observedLen=7 expectedPrefixLen=1
[DEBUG-1874] poll t=2395ms observedLen=7 expectedPrefixLen=1
[DEBUG-1874] poll t=3717ms observedLen=7 expectedPrefixLen=1
[DEBUG-1874] poll t=5097ms observedLen=7 expectedPrefixLen=1
[DEBUG-1874] wait outcome=notObserved elapsedMs=6293 route=replacement
Read it as: the field settled at 7 characters sharing exactly one character with ada@example, and never moved again. Four polls over ~4 s at a stable length. status=0 — the synthesize call reported success.
This is the dropped-middle-character family already documented in awaitSynthesizedReplacementCommitOutcome ("ada@example" -> "aexample", "Ada Lovelace" -> "Avelace"), reproduced in CI with a trace attached.
What it is not
- Not a throttle.
expectedPrefixLenis flat at 1 for the whole wait. A throttled burst walks the prefix upward; this one never progressed after the first observation. - Not the fix(ios): grant the text-entry commit wait time against progress #2035 deadline. The wait behaved correctly: progress at the first poll only, then condemned 3 s later (the 6293 ms total includes the ~1.1 s placeholder read that precedes the poll loop).
- Not the placeholder collision fixed in fix(ios): grant the text-entry commit wait time against progress #2035.
field-email's placeholder is nowType your email; the filled value shares nothing with it. - Not the failing PR's doing. ci(1874): declare the diagnose lane and stop it misreading its own loop #2059 touches CI configuration only.
Why it is worth its own issue
#1874 tracks "why does the input pipeline throttle". This is a different defect with a different signature: the characters are not late, they are wrong, and the value is stable. Grouping them would put two mechanisms behind one needs-info.
The immediately useful next step is to learn what the 7 characters actually were. The cadence log is value-free by design (#1924), so the corrupted string is not in any artifact — a one-off local reproduction on the penalized route, or a temporary opt-in that logs the observed value under a test-only flag, would say whether the loss is leading, middle or trailing, which points at RunnerSynthesizedTextEntry.replaceText's select-all-then-type sequence versus the per-character pacing above it.
Related: #1995 (the verification that caught this), #1924 (honest reporting), #1874 (same symptom, different mechanism).
Generated by Claude Code
Activity
thymikee commentedon Aug 27, 2026
Triage: valid and ready for an evidence-first agent pass.
Acceptance for the next slice:
Treat this as diagnosis, not authorization for a speculative fallback or pacing change.
thymikee commentedon Aug 27, 2026
The obvious fix is unsafe — recording why, so the next attempt does not rediscover it
I tried a single idempotent re-post of the replacement (#2086) and closed it after review. The reasoning that killed it is worth having here.
.notObservedis a collapsed outcome.awaitSynthesizedReplacementCommitOutcomereturns it for four different situations:observe()reading nil throughout, i.e. nothing resolving at the refresh point;Any retry keyed on
.notObservedfires on all four.And this route has no bound target.
synthesized-first-responder-replacementsynthesizes to the global first responder; it never resolves anXCUIElement— that is precisely the conditionshouldUseSynthesizedFirstResponderReplacementgates on (hasResolvedElement == false). A retry re-posts after a wait of up to 10 s, by which time focus may have moved. Select-all-and-write would then mutate a control the command never targeted — a worse failure than the one being avoided.So a safe retry needs three things this issue does not have yet:
All of which should follow the reproduction this issue asks for, not precede it. A retry cannot be justified without knowing whether the loss is deterministic under load — if it is, it fails twice and doubles the time.
The diagnosis still needed is unchanged and is the first thing to do: learn what the 7 characters actually were. The cadence log is value-free by design (#1924), so a local reproduction on the penalized route, or a test-only opt-in that logs the observed value, is what would say whether the loss is leading, middle or trailing — and that points at
RunnerSynthesizedTextEntry.replaceText's select-all-then-type sequence versus the per-character pacing above it.Note also that
replaceTextposts the select-all record and the text record back to back, andsynthesizeWithErrorreturns at post time. If the app has not applied the selection before the characters begin arriving, that is a candidate mechanism for a value that is neither the old nor the new text — worth ruling in or out first, since it would be a real fix rather than a retry.Generated by Claude Code
thymikee commentedon Aug 27, 2026
Triage rechecked after the #2086 retry analysis. This remains ready-for-agent only for the existing diagnosis slice: reproduce the synthesized replacement route, capture the exact stable corrupted value through test-only instrumentation, and isolate the first divergence boundary. The newer note narrows the work and is not a blocker, but it explicitly forbids a retry, pacing change, fallback, or production value logging until the mechanism is proven and target identity can be re-established safely.
ci(1874): declare the diagnose lane and read its iterations honestly
ci(1874): declare the diagnose lane and read its iterations honestly
ci(1874): declare the diagnose lane and read its iterations honestly (#…
ci(diagnose): retire the 1874 diagnose loop (#2875)
thymikee commentedon Sep 24, 2026
FYI: #2875 retired
.github/workflows/1874-diagnose.ymland its iteration script. Its default test (baretypein append mode) never reached the synthesized-replacement route this issue is about, so the loop was never a reproduction path for it. The runner still logs its[DEBUG-1874]commit-cadence lines, so the iOS smoke lane'srunner.logremains the CI evidence channel. Cadence records lengths only (#1924), so capturing the corrupted value still needs the test-only local instrumentation described in the triage above.🤖 Generated with Claude Code
test(ios-runner): assert the runner's guarantee, not the app's race
fix(ios-runner): bound the synthesized replacement pace so a value-ow…
test(ios-runner): assert the runner's guarantee, not the app's race
fix(ios-runner): slow synthesized replacement so a value-owning field…
thymikee commentedon Sep 25, 2026
Reopening. The #2904 squash commit closed this automatically, but #2904 only claims to reduce the loss. At the 40 ms acknowledge window, 60 cps left 1 of 11 characters in 20/20 bursts; 12 cps gave 7/20 exact and 13/20 with 10 of 11. The guarantee (field back-pressure) is #2906. This issue keeps the reproduction and acceptance criteria: a stable corrupted value and its first divergence.
thymikee commentedon Oct 8, 2026
Folded into #2906, which owns this guarantee: back-pressure from the field instead of a fixed characters-per-second bound. #2904 reduced the loss but didn't eliminate it, and #2086 (retry) was closed as unsafe. The trace in this issue remains the reproduction #2906 must re-run. Sibling on the element route: #2903.