[feature] the jump back to the host app is decided at runtime, and can be switched off remotely - #321
Merged
Merged
Conversation
…n be switched off remotely `HostReturn` refused to run at all on iOS 26.4+, from a compiled-in `#available(iOS 26.4, *)` gate written when the first reports said Apple had closed the path. Keyboards in the same category are observably still returning users to their host app on iPadOS 26.5, so the gate was switching off something that works — and because it was compiled in, no amount of evidence could correct it without an App Store release. The number was probably wrong. More importantly, a number is the wrong shape of answer for a private-API path whose ground truth moves with every iOS point update, in both directions. `HostReturnPolicy` replaces it with three inputs, none of which is a prediction: - **A remote flag.** `FeatureFlags.HostReturn`, fetched by `CloudClient` and cached in the App Group by `FeatureFlagStore`. `enabled: false` is a kill switch that outranks everything else, and `byOSVersion` turns one iOS off — or back on, which is the only way to re-arm devices that have given up locally. Shipping the switch in the same change as the private-API path is the point: a 2.5.1 objection can now be answered in minutes rather than in a release. - **Evidence the device collected about itself.** `attemptAndVerify` fires the launch and then watches whether the app actually got backgrounded, recording the outcome in `HostReturnLedger`. Two consecutive failures and the device stops trying. - **The OS version as a key, never as a threshold.** The ledger is stamped with the full OS build string, so an update wipes the count and the device tries again by itself — exactly when the answer might have changed. The dictation screen is optimistic and then honest: `returnableHost` is set the moment the jump is attempted and cleared again if the app is still foregrounded 1.2 s later, at which point the swipe-back guidance takes over. That 1.2 s is the entire cost of being wrong, it is only paid while the ledger is undecided, and it buys what a version check never could — a device that finds out. Fail-safe throughout. Every read is synchronous and local; the network only updates the cache. A 404 from `GET /v1/flags` is folded into "no opinion" rather than an error, so until the endpoint is published every device runs on the compiled defaults and nothing about dictation changes.
✅ SonarQube Quality Gate passed — pathorsAI_parley0 open issues on this PR. |
Lanznx
added a commit
that referenced
this pull request
Sep 2, 2026
Ships #321. 1.6.1 carried a compiled-in `#available(iOS 26.4, *)` gate that refused the jump back to the host app on the newest iOS — a number written from early reports, on devices where the path observably still works. It is now a runtime decision made from a remote flag, per-device evidence, and the OS build as a key rather than a threshold, so the answer can be corrected without a release and a device re-tries by itself after an iOS update. Build 17 because App Store Connect has already seen 16. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 2, 2026
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.
What
HostReturnrefused to run at all on iOS 26.4+, from a compiled-in#available(iOS 26.4, *)gate. This replaces that gate with a runtime policythat has a remote kill switch and learns from what actually happens on the
device.
Why
The gate was written when the first reports said Apple closed the host-return
path in 26.4. Keyboards in the same category are observably still returning
users to their host app on iPadOS 26.5, so the gate was switching off
something that works.
The wrong number is the smaller half of the problem. The larger half is that a
compiled-in answer cannot be corrected: for a path built on undocumented
internals, whose ground truth moves with every iOS point update in both
directions, a build-time constant is the wrong shape of answer entirely — and it
takes an App Store release to change.
How
HostReturnPolicy.decideanswers "attempt the jump?" from three inputs, none ofwhich is a prediction:
FeatureFlags.HostReturn.enabledFeatureFlags.HostReturn.byOSVersionHostReturnLedgerFlags are fetched by
CloudClient.featureFlags()and cached in the App Group byFeatureFlagStore, so the keyboard extension reads the same answer the app actson. The ledger is stamped with the full OS build string, so an OS update wipes
the count and the device tries again by itself — exactly the moment the answer
might have changed.
HostReturn.attemptAndVerifyis where the learning happens: it fires the launch,polls for up to 1.2 s to see whether the app actually got backgrounded, and
records the verdict.
Optimistic, then honest
returnableHostis set the moment the jump is attempted, soDictationViewshows the hand-off shape immediately; it is cleared again if the app is still
foregrounded when the grace period expires, and the swipe-back guidance takes
over. That 1.2 s is the whole cost of being wrong, it is only paid while the
ledger is undecided, and it buys the one thing a version check never could — a
device that finds out.
Why the kill switch ships in this PR rather than after it
The path this governs is private API. The realistic failure mode is not a bug,
it is App Review objecting under 2.5.1 to a build that is already out. A switch
that requires a release to reach is not a switch. This one is a server-side
field change.
Fail-safe
Nothing here can break dictation:
ever updates that cache, out of band;
GET /v1/flagsis folded into "no opinion", not into an error,so until the endpoint is published every device runs on compiled defaults and
nothing about dictation changes;
some future feature all decode to the compiled defaults rather than throwing —
a throw here is indistinguishable from "the server is down", which would
freeze the fleet on whatever it last saw.
Server-side follow-up
GET /v1/flagsdoes not exist yet. Publishing it is the only remaining step togain the switch; no client release is needed. Shape:
{ "hostReturn": { "enabled": true, "byOSVersion": { "26.4": false }, "failureBudget": 2 } }Testing
swift buildon ParleyKit is clean, no warnings.HostReturnPolicyTests,HostReturnLedgerTests,FeatureFlagsTests,FeatureFlagStoreTests— decision precedence, the budgetand its clamp, the OS-update reset, and every degradation path.
bunx tsc --noEmitclean;bunx vitest run285/285 (no TypeScript touchedhere).
swift testand the iOS app target were not run locally. This machine hasCommand Line Tools but no Xcode, so XCTest cannot link and the app target cannot
build. The ParleyKit logic was instead verified by compiling the sources into a
standalone harness asserting the same 27 behaviours (all pass), and the five
modified app-target files were checked with
swiftc -parse. CI is the firstreal build of the app target and of the XCTest suite — worth watching before
merge.
Not verified, and cannot be from here
Whether the jump actually lands, on any given iOS. That is the point of the
ledger: this stops the codebase from guessing and starts it measuring. The first
real signal will be devices on 26.5 either returning or not.
🤖 Generated with Claude Code