Skip to content

fix: prevent iOS text entry runner wedge - #1604

Merged
thymikee merged 5 commits into
mainfrom
codex/fix-ios-runner-text-entry-wedge
Aug 5, 2026
Merged

fix: prevent iOS text entry runner wedge#1604
thymikee merged 5 commits into
mainfrom
codex/fix-ios-runner-text-entry-wedge

Conversation

@thymikee

@thymikee thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Prevent the iOS XCTest runner from being torn down—or falsely reporting success—when bare text input loses its target.

The old bare type path could reach XCTest without a resolved target. XCTest then recorded a failure, invalidated the long-lived runner, and made later requests pay restart/recovery costs. This matches the archived bsky-18 restart cascade. The archived bsky-16 timeout itself ended in a model/snapshot loop, so that behavior remains a separate limitation.

This change:

  • returns typed TEXT_INPUT_NOT_FOCUSED with actionable recovery before entering the unsafe XCTest path;
  • retains a bundle/PID-bound, one-shot tapped-input witness for the immediately following bare type when the software keyboard is hidden;
  • clears that witness on lifecycle changes, non-text mutations, failed taps, and disappeared elements;
  • checks every typing chunk explicitly, so a target disappearing after partial delayed input fails instead of reporting “Typed N chars”;
  • runs no-focus, hidden-keyboard success, target-identity, and delayed target-loss regressions in the authoritative iOS lane;
  • documents the narrow query-backed XCUIElement same-identifier rebinding residual rather than adding an unreliable frame heuristic.

Validation

  • Root-cause evidence: archived runner request e6afc3fb71d6eb21 recorded xctest_recorded_failure during text entry, reported no focused input, and tore down the runner at about 89.9s.
  • Red-before hidden-keyboard proof: without the witness, the exact selector regression returned TEXT_INPUT_NOT_FOCUSED, recorded XCTest assertions, and inserted no text.
  • Red-before delayed-loss proof: without the per-chunk dispatch guard, the fixture removed the input after the first character while the command still returned success.
  • Focused live XCUITest: hidden-keyboard success and delayed target loss both passed with 0 XCTest failures.
  • pnpm build:xcuitest:ios succeeded.
  • The final fail-open pnpm check:affected --run gate passed before push at c1bb9ee79.
  • Exact-head GitHub checks are authoritative for the selected native batch.

Remaining limitations: the full paid-model benchmark cell was not rerun, and XCTest exposes query-backed elements rather than stable node-incarnation identities. The witness is therefore one-shot and guarded at every observable identity boundary, but an app-side same-identifier replacement between tap and immediate type cannot be distinguished reliably from a legitimate layout change.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-05 08:24 UTC

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.96 MB 1.96 MB 0 B
JS gzip 628.5 kB 628.5 kB 0 B
npm tarball 754.6 kB 756.5 kB +1.9 kB
npm unpacked 2.65 MB 2.65 MB +7.2 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.2 ms 27.8 ms -0.5 ms
CLI --help 67.8 ms 67.4 ms -0.4 ms

Top changed chunks: no changes in the largest emitted chunks.

@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

[P1] Preserve bare type with a valid focused responder when the software keyboard is hidden.

On iOS focusedTextInput() intentionally returns nil; bare type therefore has neither an element nor a refresh point. This new guard and the per-dispatch branch only admit it when isKeyboardVisible() sees an AX software keyboard. A simulator with Connect Hardware Keyboard enabled (or a physical hardware keyboard) has a valid focused responder but no software-keyboard element, so the documented press @ref then type "text" flow now returns TEXT_INPUT_NOT_FOCUSED instead of typing. The checked-in iOS 04-text-input-keyboard.ad replay uses that same bare path.

Please retain a safe proof for the hardware-keyboard case, and add a regression that focuses a real input with the software keyboard hidden, then proves bare type succeeds without producing an XCTest recorded failure. The existing new test covers only no app/no focus, so it cannot catch this regression.

@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

[P2] Run the no-focus wedge regression in the authoritative iOS lane

The hardware-keyboard fix now follows the production runner route and its new regression is selected by exact-head iOS CI. However, testTypeWithoutResolvedInputReturnsTypedFailureBeforeDispatchingText is still absent from .github/workflows/ios.yml, so the motivating no-focus protection is compiled but never executed in CI. Please add it beside the new hardware-keyboard case and rerun the iOS lane. That keeps both sides of this decision pinned: a tapped text input with the software keyboard hidden succeeds, while truly absent focus returns TEXT_INPUT_NOT_FOCUSED without recording an XCTest failure or wedging the runner.

@thymikee
thymikee force-pushed the codex/fix-ios-runner-text-entry-wedge branch from 6cd3ffd to a1e299c Compare August 5, 2026 06:13
@thymikee
thymikee force-pushed the codex/fix-ios-runner-text-entry-wedge branch from a1e299c to 9afcc11 Compare August 5, 2026 06:26
@thymikee
thymikee marked this pull request as ready for review August 5, 2026 06:41
@thymikee

thymikee commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Not ready: the hidden-keyboard text-entry witness is not actually scoped to the immediately following type or to the target process that produced it. invalidateCachedTarget clears lastTappedTextInput, but refreshCachedTargetIfProcessChanged replaces currentApp/PID without clearing it, and activateTarget can replace the app/bundle without clearing it. rememberedTextEntryTarget then accepts the stored XCUIElement based only on exists, so after a relaunch/process change—or after a successful bare type, which also leaves the witness reusable—a later bare type can dispatch XCUIElement.typeText without current focus proof and recreate the XCTEST_RECORDED_FAILURE runner wedge this PR is intended to prevent.

Please bind the witness to the active bundle/PID, clear it on every target-identity change, consume it after the one allowed bare type, and add an authoritative iOS regression: hidden-keyboard tap → process/bundle change → bare type must return TEXT_INPUT_NOT_FOCUSED without increasing the XCTest failure count. Also pin the one-shot consumption behavior. The existing no-focus test should assert the failure-count/runner-health invariant, not only the returned error code.

@thymikee

thymikee commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Reviewed against the bsky-18 restart-cascade evidence. The core move is exactly right: fail closed with a typed TEXT_INPUT_NOT_FOCUSED before entering the XCTest channel that records a failure and tears down the long-lived runner — turning the most expensive failure mode in the benchmark (runner teardown → restart cascade) into a cheap, actionable error. The witness lifecycle discipline is notably careful (cleared on ambiguous/offscreen/not-found/unsupported outcomes, non-text mutations, lifecycle invalidation, and element disappearance — while read-only commands like the agent's natural snapshot-between-tap-and-type preserve it), the launch-arg-gated fixture with a 1×1 inputView is a clean way to model hardware-keyboard input, and pinning both new tests into the CI -only-testing list closes the gap where in-bundle tests silently never run. The bsky-16 scoping honesty in the body is appreciated. Two findings:

P2 — mid-typing target loss now yields a silent zero-character success on bare type. The entry guard protects t=0, but typeIntoCurrentTarget returns nil when the element disappears mid-loop and the software keyboard is hidden (the new else if gate). For repairMode == .none (bare type) there is no verify step, so the command reports "Typed N chars" having dispatched nothing — the exact false-success class this whole arc is fighting (cf. the "Book TV" corruptions came from unverified typing paths). Cheap fix: track whether any dispatch actually happened across the loop and return failure: .notFocused (or a sibling code) when zero characters went out. fill's repair modes are covered by verification; bare type is the only exposed path.

P3 — witness re-resolution can cross screens. lastTappedTextInput is a query-backed XCUIElement; after a tap that triggers app-side navigation (no intervening mutating command to clear the witness), element.exists can re-resolve to a different same-identifier field on the new screen and type into it. Narrow window — most navigation goes through a clearing command — but worth a one-line comment acknowledging the residual, or capturing the element's frame at remember-time and rejecting on mismatch.

Minor: confirm TEXT_INPUT_NOT_FOCUSED gets whatever registration the error-code vocabulary expects daemon-side (the runner-client preserves unknown codes, but hint-conformance and error-audit tooling may want it enumerated). And note #1605 edits the same Lifecycle.swift/CommandExecution.swift regions — whichever lands second needs a careful rebase, particularly around invalidateCachedTarget.

🤖 Addressed by Claude Code

@thymikee

thymikee commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Addressed the mid-command false-success finding at exact head 4e3315b02. typeIntoCurrentTarget now reports dispatch explicitly and every requested chunk fails immediately with TEXT_INPUT_NOT_FOCUSED when the target disappears, including bare delayed type with the software keyboard hidden.

The regression uses a real iOS fixture that removes the text field after the first editing event. Red-before, the command returned success after dispatching only the first character; restored, the delayed-loss test and hidden-keyboard success test both pass (2 tests, 0 failures), the runner records no XCTest failure, the iOS XCTest build succeeds, and pnpm check:affected --run passed before push.

The same-identifier cross-screen query re-resolution observation remains a narrow residual: the witness is one-shot and bundle/PID bound, but XCTest does not expose a stable element-incarnation identity that would let us reject that case without also rejecting valid layout changes. I am not claiming that residual is eliminated by this head. Exact-head CI is running.

@thymikee

thymikee commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Follow-up exact head c1bb9ee79 closes the authoritative-lane gap: testBareDelayedTypeFailsWhenTappedInputDisappearsMidCommand is now selected explicitly in .github/workflows/ios.yml. The query-backed XCUIElement same-identifier rebinding limitation is documented at the witness decision site; no frame heuristic was added because it would reject legitimate layout changes without proving element incarnation. The full fail-open affected gate passed before push; exact-head iOS CI will now execute the regression.

@thymikee

thymikee commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Re-reviewed exact head c1bb9ee79d9598e27ee6bc6150cfea57508ed67f. The delayed-loss regression is now selected by authoritative iOS CI; the per-chunk dispatch fix fails closed without recording an XCTest failure. The query-backed same-ID rebinding case is accurately documented as a narrow residual, with one-shot and observable identity boundaries intact. No remaining actionable code finding; readiness depends only on exact-head CI completing.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 5, 2026
@thymikee
thymikee merged commit 4c7a899 into main Aug 5, 2026
30 checks passed
@thymikee
thymikee deleted the codex/fix-ios-runner-text-entry-wedge branch August 5, 2026 08:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant