You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ios: fill's synthesized-replacement route lands a corrupted value (traced: 11 chars requested, 7 observed, frozen) #2080
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).
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.expectedPrefixLen is flat at 1 for the whole wait. A throttled burst walks the prefix upward; this one never progressed after the first observation.
#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).
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.
.notObserved is a collapsed outcome.awaitSynthesizedReplacementCommitOutcome returns it for four different situations:
a placeholder collision — returned before any polling, where the field may hold exactly the right text;
observe() reading nil throughout, i.e. nothing resolving at the refresh point;
a stable wrong value — this issue;
nothing ever arriving.
Any retry keyed on .notObserved fires on all four.
And this route has no bound target.synthesized-first-responder-replacement synthesizes to the global first responder; it never resolves an XCUIElement — that is precisely the condition shouldUseSynthesizedFirstResponderReplacement gates 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:
a typed outcome that distinguishes a stable corrupted value from the other three cases;
re-establishment of the same target before re-posting, not an assumption that focus held;
coverage that plants both halves red: a wrong stable first value followed by the exact second value, and a changed/lost focus refusing the retry.
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 replaceText posts the select-all record and the text record back to back, and synthesizeWithError returns 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.
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.
FYI: #2875 retired .github/workflows/1874-diagnose.yml and its iteration script. Its default test (bare type in 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's runner.log remains 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.
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.
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.
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.