Skip to content

perf(ios): read each resolved text input through one snapshot - #3882

Open
thymikee wants to merge 7 commits into
mainfrom
t3/ios-fill-performance
Open

thymikee wants to merge 7 commits into
mainfrom
t3/ios-fill-performance

Conversation

@thymikee

@thymikee thymikee commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

Each attribute read from a live XCUIElement re-runs its query, and each query snapshots the whole app. A fill made 35 queries. Now:

  • Every resolver returns ResolvedTextEntryInput (element + snapshot). It owns the exists-first and snapshot-first reads. Value, frame, identity, and the baseline come from that snapshot. The point lookup's snapshots also answer identity, so its consumers stop probing again.
  • TextEntryReader owns reuse: input() hands out the last read until dispatch { } runs, which wraps every post, tap, and pause. The focus step's post-focus read seeds it. A bound target refuses a replacement input after any dispatch.

Read-back, unconfirmed evidence, identity refusal, the tap witness, textEntryRoute, and the penalized route are unchanged. 12 runner files. The fixture gains a value-read counter and an on-demand input replacement.

Validation

  • pnpm check:affected --run --head 737fa35cbc: passed, exactCommit: true. GitHub owns the Swift and smoke lanes.
  • 121 runner tests (text entry, input probe, typing, command execution, interaction) pass at 737fa35, iPhone 17 Pro iOS 27.0 simulator.
  • The query-count test reads 35 on main, 19 at 2d34505, and 17 now (pin ≤ 24).
  • Mutation: a warmup pause that bypasses the reader fails testFillFailsWhenAppReplacesMaskedInputWhileWarmupPauses (types 23456 into the successor).
  • Runner type median ms, interleaved, 2d34505 → 737fa35: test-app field-email 3483 → 3280; Bluesky Sign in 2910 → 2767.

Risk: a fill can reuse a read only between dispatches; readiness's slow path pays one extra snapshot.

🤖 Generated with Claude Code

View guided diff Turn on auto-fix

On iOS every attribute read from a live XCUIElement re-runs its query, and
each query snapshots the whole app (~90-110 ms on Bluesky's React Native
Search screen). A fill read identity, frame, and value one attribute at a
time and resolved the field again right before each post, so it made ~37
queries.

- Resolution returns the snapshot it read; value, frame, identity, and the
  replacement baseline come from that snapshot.
- The focus step hands its post-focus read to the entry, and a post reuses
  the last read when nothing was dispatched since it was taken. After a
  clear, the field resolves again under its bound identity.
- The point lookup runs one query for all four text input types, reads each
  candidate through one snapshot, and still hands out per-type handles.

Bluesky Search fill, runner type: 5.6-6.2 s -> 3.3-3.7 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.53 MB 5.54 MB +5.6 kB
Package (unpacked) 5.53 MB 5.54 MB +5.6 kB
Package (download) 1.68 MB 1.68 MB +1.2 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 41.0 ms 38.5 ms -2.5 ms
CLI --help 79.3 ms 80.4 ms +1.1 ms

Two reuses from the one-snapshot change could pair a read with the wrong
input:

- The point lookup counted per-type positions across candidates read from
  a shared query that each re-ran it. An input that left the app between
  two candidate reads shifted the later positions, so a handle could name
  another input than the snapshot it carried. Each handed-out handle now
  reads as the candidate's identity and frame; on a mismatch the lookup
  runs the per-type queries, whose handles read through their own
  snapshots.
- The warmup kept its read across the pause it takes for an unreadable
  value or an expired deadline, so the next post skipped the identity
  check and could type into a successor. Only a read that no pause
  follows is reused now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member Author

Both review findings are fixed in 6711463 (pushed on top of 8894c98, no force-push).

P2-1 — snapshot attached to the wrong per-type handle. Fixed. The point lookup still reads candidates through the shared elementType IN query. Each per-type handle it hands out is now snapshotted and must match the candidate's element type, identifier, and frame. The handed-out input carries the handle's own snapshot. On a mismatch, the shared query changed during the lookup, so the lookup runs the four per-type queries instead (the pre-PR shape, one snapshot per handle) and logs AGENT_DEVICE_RUNNER_TEXT_INPUT_LOOKUP_BY_TYPE.

  • New fixture --agent-device-mixed-text-inputs: a text field ahead of two search fields (UISearchBar). A bare UISearchTextField reads as a text field, and the shared query ordered the search fields after the text fields, so the reviewer's exact [search, text, text] layout couldn't shift anything. This fixture has the same shape with the types swapped.
  • New test testTextInputLookupKeepsItsInputWhenAnEarlierInputOfAnotherTypeLeaves removes the text field after the lookup reads candidate 0, through a new unit-test-only hook textInputCandidateReadForTesting.

P2-2 — warmup kept identity evidence across a sleep. Fixed. The warmup keeps its read for the next post only when it returns without pausing (a readable, non-pending value). Both the unreadable-value pause and the deadline-expiry path leave undispatchedRead nil, so the next post resolves under its bound identity.

  • New fixture flag --agent-device-text-entry-replace-on-request: a button that performs the existing replace-after-input navigation on demand.
  • New test testFillFailsWhenAppReplacesMaskedInputWhileWarmupPauses presses that button from a new unit-test-only hook, textEntryWarmupPauseForTesting, which runs right before the warmup pause.

Fail on 8894c98. I ran both tests against 8894c98's production code with only the two hooks inserted. Both failed for the intended reasons:

  • testTextInputLookupKeepsItsInputWhenAnEarlierInputOfAnotherTypeLeaves: the handle was mixed-search-a while the snapshot was mixed-search-b.
  • testFillFailsWhenAppReplacesMaskedInputWhileWarmupPauses: the fill returned ok, with no TEXT_INPUT_NOT_FOCUSED, and the successor held 23456.

Both pass on 6711463.

Validation at 6711463

  • 116 runner tests across the TextTyping, TextInputProbe, TextEntryLatency (including the ≤24-query pin), CommandExecution, CoordinateTextEntry, TextEntryConfirmation, SynthesizedTextEntry, TextEntryPolicy, TextInputCandidatePolicy, Keyboard, Interaction, and TapPointPolicy suites: 0 failures, on an iPhone 17 Pro iOS 27.0 simulator.
  • pnpm check:xctest-selection: passed.
  • pnpm check:affected --run --head 67114638bb: passed, exactCommit=true. Swift runner lanes are GitHub-authoritative.
  • No new runner request fields.

Bluesky 1.136.0 Search fill, 'react native' from the Explore screen, runner type accepted→completed in ms. All three builds ran on the same simulator, and I checked each installed runner binary:

c8b944c (main) 8894c98 6711463
4378 / 4353 / 4339 2919 / 2909 / 2892 2965 / 2978 / 2987

Cost of the fixes: about +70 ms per fill, all in the focus phase (1048–1059 → 1115–1132 ms). That is the extra snapshot that validates the point-lookup handle. The by-type fallback never triggered on Bluesky.

Two notes:

  • Absolute numbers are lower than the PR's original table (5.9 → 3.5 s), which came from another simulator and screen state. The relative gain holds: −33 % vs main.
  • Remaining cost: if a handle names a position that no longer exists after a mid-lookup removal, its validation snapshot waits about 2 s before it fails over to the by-type lookup. This happens only in that race.

@thymikee

Copy link
Copy Markdown
Member Author

At 6711463, the point lookup in RunnerTests+TextInputProbe.swift can still return the wrong input when the shared query shifts. I did not run the XCTest suites or the query-count test, and I did not see this shift happen live. The finding comes from reading the code.

Each candidate in the shared elementType IN query is bound by index and resolves again on every read (https://github.com/callstack/agent-device/blob/6711463/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputProbe.swift#L195). The shift check, isSameTextInput, runs only after isCoordinateTextInputCandidate accepts a candidate. A candidate that no longer exists is skipped with continue. Take [text0, searchA at P, searchB]. If text0 leaves after position 0 is read, position 1 now reads searchB, which is not at P, so it is filtered out with no check. Position 2 no longer exists and is skipped. searchA is never read. Insertion ahead of the input has the same effect, because the input moves past the captured count. A fill or type at P then returns no input at the point, or a larger input that encloses it, and the lookup does not fall back. Users see a spurious "no text input found at the provided coordinates" or a raw-point focus. Before this PR, the searchFields query was not affected by a text field leaving. This is the same cross-type race the earlier fix targeted, now reached through a non-matching or vanished position. Every position of the shared query must name the input it named when the lookup started, for every candidate read, not only for candidates at the point. Please enforce that in queryTextInputs before the point filter. Treat a position that no longer exists as a changed query and fall back to queryTextInputsByType. Also confirm the query count is unchanged after the loop, or validate every existing candidate's handle before filtering. Please add a fixture case where the input at the point is the one that shifts away.

Could the lookup use queryTextInputsByType alone? It already exists at head as the fallback. It keeps the four per-type queries and takes one snapshot per candidate. Per-type handles never shift across types. That would delete the shared elementType IN query, the indexByType binding, the per-handle validation snapshot, isSameTextInput, textInputCandidateReadForTesting, the mixed-input fixture, and this whole race class. It costs about three extra queries per lookup, which the author measured at about +70 ms. The snapshot-per-resolution and undispatched-read parts look sound and should stay. A Bluesky measurement of the shared query plus validation against per-type only would show whether the shared query still pays for its race handling.

Not blocking, and you can take or leave it: for an unbound target, resolveTextEntryInput discards the snapshot that textInputAt already read at the refresh point, because resolveUnboundTextEntryElement returns refreshed.element (RunnerTests+TextEntry.swift#L344). Returning ResolvedTextEntryInput from it and passing the lookup's snapshot through would avoid the second snapshot.

All 16 checks pass, including the Swift runner lanes that exercise the changed path, and there are no conflicts. The claims "fails on base 35 > 24" and "fails on 8894c98" are the author's, and I did not reproduce them. The Bluesky timings are also the author's, from an iOS 27.0 simulator, and there is no physical-device run. Before merge, the point lookup's shift check must cover every shared-query position, including vanished and non-matching ones, or the shared query must give way to the per-type lookup.

The point lookup validated a handle only for a candidate at the point,
after the point filter. An input of another type that left the app after
an earlier position's read moved a later input into a position already
read, so the input at the point was never read: the next position read
an input elsewhere, which the filter dropped, and the last position no
longer existed and was skipped. The lookup returned no input without
falling back, so a resolved-coordinate fill failed with
NO_TEXT_INPUT_AT_POINT and a coordinate tap could lose its typing
witness.

Every position now must still exist and every handle must read as its
position's input, at the point or not; otherwise the lookup runs the
per-type queries. A position's existence is read only when its snapshot
fails, so a lookup reads each candidate twice, as before for the input
at the point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member Author

Round 2, P2 (RunnerTests+TextInputProbe.swift): fixed in bef8706.

Finding. The lookup checked a handle only for a candidate at the point, after the point filter. Take [text0, searchA at P, searchB] and remove text0 after position 0 is read. Position 1 then reads searchB, and the filter drops it. Position 2 no longer exists and was skipped. The lookup returned nothing without falling back.

Fix. The checks now run before the point filter, for every position:

  • If a position no longer exists, the lookup falls back to the per-type queries. Existence is read only when the position's snapshot fails.
  • If a handle does not read as its position's input (type, identifier, frame), the lookup falls back too, whether or not that input is at the point.

Each candidate still takes two reads: the position snapshot and the handle snapshot. The exists read that came before the snapshot is gone, so a candidate at the point costs one read less and a candidate elsewhere costs the same. The ~2 s XCTest wait now also applies when a position goes missing, and only in that race. The trace for the vanished case shows the wait from 5.15 s to 7.21 s, then the fallback.

Tests (RunnerTests+TextInputProbeTests.swift). All three cases share one helper that removes mixed-text after the lookup reads a given position:

  • testTextInputLookupFindsTheFirstSearchFieldWhenTheTextFieldLeavesAfterTheFirstRead: targets mixed-search-a and removes after position 0. This is your scenario and covers the mismatch-before-filter branch.
  • testTextInputLookupFindsTheSecondSearchFieldWhenTheTextFieldLeavesAfterTheSecondRead: targets mixed-search-b and removes after position 1. This covers the vanished-position branch.
  • testTextInputLookupKeepsItsInputWhenAnEarlierInputOfAnotherTypeLeaves: the round-1 case, kept. It pairs a handle with the wrong snapshot when handle validation is missing.

The helper also asserts that no XCTest failure was recorded. The test hook now runs after a position's handle check.

On 6711463's RunnerTests+TextInputProbe.swift with the new tests, on an iPhone 17 Pro iOS 27.0 simulator:

  • Both new cases fail with XCTUnwrap failed: expected non-nil value of type "ResolvedTextEntryInput", which is the lookup returning nothing.
  • The round-1 case passes.

At bef8706, all three pass.

Validation at bef8706

  • 125 runner tests pass, 0 failures, on the same simulator: text-input probe, text entry (confirmation, policy, latency), typing, coordinate and synthesized text entry, candidate policy, command execution, interaction. They ran in two batches of 106 and 19, because my first run hit my own tool's time limit. That includes testFillReadsEachResolvedFieldThroughOneSnapshot, the query budget test.
  • pnpm check:xctest-selection passed.
  • pnpm check:affected --run --head bef8706f01 passed with exit 0 and exactCommit=true. It ran xctest-selection and packaged-runner-swift; the Swift build and device lanes are GitHub-owned.
  • pnpm gate unit-ci failed on this host. The load average was about 1000 from other workers. The first run had many provider-integration timeouts. A solo rerun had 5 timeout or deadline failures. I then ran the same files on unchanged 6711463 and on bef8706, back to back. Both give the same two failures: macos-helper-package-contents ("timed out in 5000ms") and apple-platform-output-guard ("record did not return within 20s"). The other three passed on both. This commit changes only Swift runner files. I'm treating the failures as base-equivalent host contention; CI owns the gate at this head.

Timing. The hot path changed, so I re-measured. Bluesky 1.136.0, Search react native from Explore, one simulator, one 6711463 runner and one bef8706 runner, runs interleaved head → base → head. Values are ios_runner_command_send ms:

6711463 bef8706
3457 (first after open) / 3123 / 3105 3164 / 3096 / 3127, then 3641 (first after reopen) / 3100 / 3184

There is no measurable difference. Absolute numbers aren't comparable with the PR table, because the host was under heavy load and the metric is different.

🤖 Generated with Claude Code

The shared elementType IN query saved three queries per point lookup but
needed index rebinding, a validation snapshot per candidate, and a
fallback for every shift race. Per-type handles never shift across types,
so the lookup now reads each type's query and snapshots each candidate
once, reading existence only when the snapshot fails. Bluesky and
test-app fills measured no slower.

An unbound target that resolves through its refresh point now keeps the
snapshot the point lookup read instead of reading the input again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member Author

Round 3: both points are answered in 2d34505 (fast-forward on top of bef8706).

Can the lookup use the per-type queries alone? Yes, and it now does. I measured first. On one iPhone 17 Pro iOS 27.0 simulator, I compared two runners:

  • bef8706: the shared query plus validation.
  • A per-type-only variant.

Runs alternated between the runners: 3 rounds per runner, each with 1 warm-up fill and 3 measured fills. Before each block, I stopped the daemon with --clean and checked which runner binary was installed. Values are the ios_runner_command_send median in ms, n = 9 each:

Fill shared + validation per-type only
Bluesky 1.136.0, logged-out Sign in username field, 'react native' 2800 (2770–2941) 2817 (2772–2947)
Test app Checkout form field-email, qa@example.com 3427 (3382–3658) 3355 (3340–3484)

Logged-out Bluesky has no Explore or Search, and I did not sign a real account into a fresh simulator. The Sign in form is the Bluesky screen I measured.

The per-type lookup now snapshots each candidate first. It reads exists only when the snapshot throws. Each candidate costs one read instead of two, which pays for the three extra type queries. testFillReadsEachResolvedFieldThroughOneSnapshot reads 19 values at 2d34505 and 20 at bef8706. The ≤ 24 pin is unchanged.

This deletes:

  • The elementType IN query and the indexByType binding.
  • The validation snapshot and isSameTextInput.
  • The queryTextInputsByType fallback and its log line.
  • textInputCandidateReadForTesting.
  • The --agent-device-mixed-text-inputs fixture and the three mixed-input tests.

The snapshot-per-resolution and undispatched-read parts stay. A same-type shift is still possible, as it is on main.

Unbound snapshot pass-through: taken. resolveUnboundTextEntry now returns either .element or .lookup(ResolvedTextEntryInput):

  • resolveTextEntryInput returns the lookup's input with its snapshot.
  • resolveTextEntryElement reads no more than before.

The new test is testUnboundResolutionReadsItsInputThroughThePointLookup. It resolves an element-less unbound target at the field's point and asserts that the resolution reads no more than textInputAt alone does. On bef8706 it fails: ("5") is not equal to ("3"). At 2d34505 it passes.

Validation

  • 116 runner tests pass at 2bbbcd873b. 2d34505 only removes one blank line from a test file. The tests cover text typing, input probe, text-entry latency, confirmation and policy, coordinate and synthesized text entry, candidate policy, keyboard, interaction, tap-point policy, and command execution.
  • pnpm check:xctest-selection passed.
  • pnpm check:affected --run --head 2d345058e9 passed with exit 0 and exactCommit=true. It ran xctest-selection and packaged-runner-swift. The Swift lanes are GitHub-owned.
  • An independent read-only review found no blocking issues.

🤖 Generated with Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

The PR is ready. At 2d34505 the findings from the earlier review are fixed, and I found no new problems. Not blocking: the per-candidate read in RunnerTests+TextInputProbe.swift (line 182) takes the snapshot before checking exists, while ResolvedTextEntryInput(reading:) and textEntrySnapshot in RunnerTests+TextEntry.swift check exists first, so you could add a second named initializer (for example snapshot-first) to make the type own that choice, or leave it as is since both sites document the trade-off.

I ran no XCTest or runner suites. The before-and-after numbers for the new regression test (5 vs 3) and the 19-value query budget are yours. The Bluesky and test-app timings come from your iPhone 17 Pro iOS 27.0 simulator, and there is no physical-device run. The same-type index shift on removal still exists, as it does on main, so it is not a regression.

No conflicts. Lint, Typecheck & Package fails only on the line cap for packages/platform-apple/src/snapshot-route.test.ts (1003 lines). That failure comes from main and #3890 fixes it, and this PR's diff does not touch that file. Rebase onto main once #3890 lands so that check goes green, and then the PR can merge.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 22, 2026
thymikee and others added 3 commits October 22, 2026 10:14
* origin/main:
  feat(test-app): add a Bluesky-depth heavy feed screen (#3884)
  fix(android): read a child that vanished mid-read as gone (#3901)
  feat(ios): route iPad Simulator snapshots through the host AX bridge (#3895)
  Prepare shared resource ownership and selection for #3325 (#3887)
  refactor(kernel): track held execution locks in keyed-lock (#3879)
  perf(ios): ask a ready runner about surface hosts before spawning probes (#3877)
  fix(ios): keep the bridge while a surface host only lingers (#3873)
  perf(ios): synthesize taps on resolved in-app back controls (#3880)
  fix(macos): open a running app by its own path instead of its bundle id (#3889)
  fix(macos): accept mixed-case bundle ids in open (#3867)
  fix(macos-helper): scroll SwiftUI forms whose rows are scroll-area children (#3888)
Every resolver now returns ResolvedTextEntryInput, which owns the
exists-first and snapshot-first reads. TextEntryReader hands a read out
again until the next dispatch or pause, replacing the post-focus and
undispatched reads tracked at each call site. The point lookup's
snapshots answer identity, so its consumers stop probing candidates
again. A fill reads the counted field 17 times instead of 19.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Append verification, the repair decision, and the repair itself read
through TextEntryReader, so a verification starts from the read its
check took and the repair dispatches through the reader. The clear's
caret tap takes its frame from the read it clears. A read that raises
logs its exception, and every input comes from a named read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

thymikee commented Oct 22, 2026 •

Copy link
Copy Markdown
Member Author

Redesign: one read type, one owner for reuse. Two commits on top of 2d34505 (merged with main first, no force-push): 90fa513 and 737fa35.

Design note

  • ResolvedTextEntryInput is the only resolution result. Every resolver returns it: element + snapshot, with identity and attributes computed from the snapshot. The type owns the read choice through two named initializers, as the round-3 note asked:
    • init?(reading:): exists-first, for a handle that may name a removed input.
    • init?(readingListed:): snapshot-first, for a handle its query just listed.
  • TextEntryReader owns reuse. It holds the target and its last read. input() returns the last read, or resolves the target and moves it to the new read. dispatch { } wraps every post, tap, clear, repair, and pause, and drops the read. The focus step's postFocusRead seeds the reader. Call sites no longer reason about staleness. Replacement confirmation, append verification, and repair poll through the reader too.

Replaces resolveTextEntryElement, UnboundTextEntryResolution, the snapshotTextEntryInput tuple, textEntrySnapshot and textEntrySnapshotFrame, withElement and retargeted, undispatchedRead, typedTarget, the re-resolves after app-wide typing, the warmup and end-of-plan retargets, the clear's explicit re-resolve, and the second caret-frame read.

Deleted elsewhere. The point lookup's snapshots now answer identity, so coordinateTapTextInputIdentityAt, the bound point fallback, and boundTextEntryInputIsGone no longer probe each candidate again. SynthesizedCommitDeadline reads the placeholder and value from the snapshot. Append verification resolves once before it verifies.

Kept. Two pieces of the fixture stay:

  • The app-side value-read counter. The test target has no public hook to count queries; swizzling private XCTest would be the only other way.
  • The on-demand replacement. The round-1 warmup-pause test needs it.

Cost. Readiness's slow path, where focus was not confirmed, pays one extra snapshot. That is the price of a single resolver.

Numbers

  • Diff vs main:
    • Before (2d34505): +460/−129, of which production +265/−121.
    • Now: +489/−236, of which production +294/−228. Net production growth goes from +144 to +66 lines. The test and fixture lines are unchanged.
    • The size report will update on this push.
  • Query-count test: 35 on main, 19 at the old head, 17 now. The count was identical over 2 runs. Pin ≤ 24.
  • Runner type (ios_runner_command_send), median ms. Interleaved head ↔ new on one iPhone 17 Pro iOS 27.0 simulator, --clean daemon stop between blocks, and each block's installed runner binary checked:
Fill a430292 (old head + main) 737fa35
Test app Checkout field-email, qa@example.com 3483 (3244–3737, n=12) 3280 (3211–3719, n=9)
Bluesky 1.136.0 logged-out Sign in username, 'react native' 2910 (2801–3170, n=9) 2767 (2639–2915, n=9)

One test-app block for the new runner was discarded. Its press label="Form" ran before the app was ready, so the 4 fills never reached the form. All other fills succeeded.

Validation

  • 121 runner tests pass at 737fa35 (0 failures). They cover text input probe, text entry (confirmation, policy, latency), typing, coordinate and synthesized text entry, synthesized commit deadline, candidate policy, keyboard, interaction, tap point policy, and command execution.
  • Mutation at 90fa513: the warmup pause skips reader.dispatch. testFillFailsWhenAppReplacesMaskedInputWhileWarmupPauses then fails: there is no TEXT_INPUT_NOT_FOCUSED, and the successor holds 23456.
  • pnpm check:xctest-selection passed.
  • pnpm check:affected --run --head 737fa35cbc passed: exit 0, exactCommit=true.
  • Independent read-only review of 90fa513: no P1 or P2 findings.
    • P3 notes taken in 737fa35: append verification reuses its check's read, the repair dispatches through the reader, the caret frame comes from the clear's own read, init?(reading:) logs a raised exception, focused inputs use the caught read, the base initializer is private, and stale comments are fixed.
    • P3 accepted as-is: the extra readiness snapshot.

🤖 Generated with Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

This PR is ready at 737fa35. The redesign since my earlier review at 2d34505 holds up: one snapshot per read, reused only until the next dispatch, and I found no code problem. All 16 checks pass, including the Apple Smoke Tests job, which only covers the iOS runner build with the changed text-entry files. Its XCTest lane ran 4 unrelated tests, so CI does not exercise the changed path. The 121-test pass, the 17-query count, the warmup-pause mutation and the test-app and Bluesky fills are your simulator results (iPhone 17 Pro, iOS 27.0), and I ran no runner or device myself. Not blocking, and fine to take or leave: init?(reading:) at RunnerTests+TextEntry.swift:223 inlines the exception catch and log that RunnerTests.safely already does, so a small static helper next to safely could serve both.

This branch has not been deployed

No deployments
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