refactor: migrate back/home/orientation/tv-remote/keyboard to the request-bound device runtime - #1955
refactor: migrate back/home/orientation/tv-remote/keyboard to the request-bound device runtime#1955thymikee wants to merge 12 commits into
Conversation
…uest-bound device runtime Continues the ADR 0019 platform-runtime migration (Wave 5 generic leaves): five generic-route commands move off dispatchKnownCommand/Interactor legacy dispatch onto fact-owned admission, one bind per handler. keyboard uses the R35 action-selected single-bind pattern (status/dismiss/enter each admit and bind independently). All 8 owner runtime packages gained fact-cell tests for the new operations; six smoke-coverage integration oracles and nine daemon/capability unit test files were updated for the retired capability- catalog admission these commands no longer carry.
fda3e35 to
e1e34ae
Compare
Size Report
npm unpacked components
Startup median (7 runs, lower is better):
Top changed chunks:
Top changed packed files
|
…ntime bindKeyboardStatus/Dismiss/Enter repeated the same signal-check + resolveInteractor call; factor it into resolveKeyboardInteractor so each binder is a two-line call instead of a six-line copy. No behavior change — the three contract-module mutants planted earlier in review still kill on this shape.
Review findings[P1] Close the watchOS sentinel before declaring these facts. The Apple interactor explicitly rejects watchOS because it has no XCUITest backend, but the new Apple facts admit back, home, orientation, and keyboard dismiss/enter. Those commands now bind and fail only when resolving the interactor; the retired Apple capability table refused home, keyboard, and orientation on this sentinel. Mark every interactor-backed operation unavailable for watchOS and make the leaf test assert no binding, so facts remain the support authority. [P2] Preserve the tv-remote target-mismatch response. Before this cutover, non-TV targets received the shared TV-target message and cross-target selector hint. Exact-fact admission now returns a generic refusal and owner-specific hints instead. Keep facts as the support authority, but preserve the existing daemon response and pin iOS plus Android-mobile parity. Validation is currently red on this head: Coverage fails the new orientation router test (its focused run passes, which does not resolve the full-suite failure), and Layering Guard fails the unrelated tmpdir child-liveness test. Both need a green CI rerun; the Coverage failure needs diagnosis if it recurs. |
|
Not merge-ready at exact head P1: Apple facts admit the explicitly unsupported watchOS sentinel. P2: CI: Coverage has one real owner-action failure in |
… tv-remote non-TV parity P1: watchOS has no constructible Apple interactor (XCUITest cannot drive its UI, ADR-0009), matching the existing captureScreenshot/captureSnapshot/readTextAtPoint/findSelector pattern in this same file. appleBackFact/appleHomeFact/appleMobileInputEligible admitted every Apple OS but tvOS/macOS, wrongly including watchOS. Facts now refuse watchOS explicitly for back, home, orientation, and keyboard dismiss/enter, with a fact-cell test asserting no binding for every one of them. P2: verified the daemon's generic-route capability gate already reproduced the retired per-platform tv-remote hint text (message stays the generic "<command> is not supported on this device", hint carries the owner-specific text) for every device that could reach dispatch in the old system -- the retired handleTvRemoteCommand's own "supported only on TV targets" check was unreachable there and only exercised by a test calling dispatchCommand directly. Added a daemon-level test pinning the exact iOS and Android-mobile hint strings to make that parity explicit instead of implicit. Also fixes a fallow complexity finding the P1 test edit introduced by splitting the fact-cell assertions into five small named helpers instead of one large function.
|
Thanks for both passes — pushed P1 (fixed): confirmed — P2 (investigated, no change): traced the retired code path all the way through. So the current fact-based admission already reproduces old production parity byte-for-byte for every reachable case. Rather than reintroduce the (dead) unified message, I added a daemon-level CI: Layering Guard's failure is |
Deep code-quality reviewVerified green at The headline: this PR is the moment five of these leaves stop being a coincidence and become a missing abstraction, and the diff rearranges that duplication rather than deleting it. Three of the findings below are the same shape — boilerplate that got a wrapper instead of a model. 1. Seven copies of "fact-keyed table of interactor binders" — this is the code-judo moveThe same body now exists in seven places:
Every one of those bodies is 100% contracts vocabulary: And the justification attached to those suppressions doesn't hold:
ADR 0019 forbids a shared package between sibling platform packages. It does not forbid contracts, which every one of these packages already imports, and which already owns both halves of what's being duplicated: the binders and the fact keys. This isn't a shared-mechanics problem — there are no mechanics here. I prototyped the fix and it typechecks clean. Contracts exports a return bindAdmittedLocalInteractorOperations({
device: request.device,
signal: request.scope.signal,
resolveInteractor: host.localInteractors.resolve,
facts,
operations: ['back', 'home', 'setOrientation', 'tvRemote',
'keyboardStatus', 'keyboardDismiss', 'keyboardEnter'],
});Apple keeps its bespoke Related, same root cause: 2.
|
… real adb Root-caused the CI-only Coverage failure (unreproducible locally in isolation, reproducible 2/2 in the full CI run): every generic-route leaf this migration touches carries `androidBlockingDialogGuard: true`, and `dispatchGenericCommand` calls `ensureNoAndroidBlockingDialogReady` unconditionally for any `platform: 'android'` session reaching the real request router -- regardless of whether admission is fact-based or capability-based. That check calls `getAndroidBlockingDialogFocus`, which shells out to the real `adb` binary. orientation-runtime.test.ts's "request router joins..." test used a synthetic `platform: 'android'` device through `createRequestHandler` (the real router), without stubbing the platform ADB layer -- only the runtime gateway was mocked. On a host with a real `adb` binary (my machine) the subprocess fails fast and `allowFailure` tolerates it, costing ~800ms-1.1s but still succeeding. On a host with no `adb` binary at all (CI's Coverage job, a plain unit-test lane with no Android SDK) the spawn itself throws, which isn't something `allowFailure` catches, producing exactly the observed `ok: false` unsupported-operation response. back/home/tv-remote's equivalent router-join tests already use Apple/Vega devices, so they never reached this path. Switched orientation's fixture to match -- Apple, since the fixture's facts/execution are fully synthetic and platform-agnostic regardless. Also: renamed the widely-shared 'emulator-5554'/'ios-simulator' device-id literals in back/orientation/tv-remote/keyboard-runtime.test.ts to file-scoped ids. Device claims for a `local-family` owner binding hit the real on-disk `require-owner` claim file (keyed only by canonical device id), and 27+ pre-existing test files already share 'emulator-5554'; this migration added three more consumers of it under a `require-owner` policy that reaches real admission, which was worth eliminating as a source of doubt even though it wasn't the actual root cause here.
|
Root-caused the Coverage failure — it was real, not a flake (reproduced 2/2 in CI). Pushed
On my machine (real
While tracing this I also found (and fixed) that |
|
Prior correctness blockers are fixed and exact-head CI/device evidence is strong, but this is not ready yet. First, the migration adds about 1,921 net production lines and +15.3 kB unpacked; |
…test the real Android dialog-guard path packages/platform-apple/src/runtime.ts and packages/provider-limrun/src/app-log-runtime.ts grew past the repo's 500-line extraction threshold. Move the new back/home/orientation/ tv-remote/keyboard facts and bindings into packages/platform-apple/src/navigation/runtime.ts (new sibling module, matching deployment/runtime.ts's existing pattern), and the new keyboard facts/bindings for limrun into the existing packages/provider-limrun/src/interaction-operations.ts (which already held the sibling navigation logic). Also fix orientation-runtime.test.ts's router-join test: it previously swapped its device fixture from Android to Apple to dodge the real adb-backed blocking-dialog guard, which masked the Android route that was actually failing in CI. Keep the Android fixture and stub getAndroidBlockingDialogFocus instead, the same seam request-router-android-modal.test.ts already uses.
|
Both fixed — pushed Size/extraction (fixed): Moved the new back/home/orientation/tv-remote/keyboard facts and bindings out of the two owning-facet files into sibling modules, matching the pattern the repo already uses (
Both owning files are now well clear of the 500-line extraction threshold, and both sibling modules are well under the 300-line target. No further growth to itemize/justify — the remaining diff is the fact-cell logic itself (five new operations across six owner packages), which has nowhere further to shrink without changing what's being admitted. Coverage masking (fixed): Agreed — swapping the fixture to Apple proved the wiring but stopped exercising the Android route that actually failed.
|
…equest-bound-migration-803b60 * origin/main: perf(contracts): granularize entry surfaces so hub importers stop evaluating the facade clump (#1969) fix(ios): preserve regular snapshot depth through structural wrappers (#1947) diagnose(1874): instrument the synthesized commit wait and add a dispatchable stall loop (#1941) refactor(tests): replace the test-utils barrel with direct module imports (#1956) fix(ios): enforce regular snapshot clip invariant (#1946) refactor(daemon): lazy-load platform cleanup helpers in session teardown (#1950) perf(typecheck): make the root tsc project incremental (#1957) docs: drop stale apps.ts over-budget warning from AGENTS.md (#1954) refactor: dedupe containsPoint and rectArea into @agent-device/kernel (#1953) # Conflicts: # packages/platform-android/src/runtime.ts # packages/platform-harmonyos/src/runtime.ts # packages/platform-linux/src/runtime.ts # packages/platform-vega/src/runtime.ts # packages/platform-web/src/runtime.ts # packages/provider-limrun/src/app-log-runtime.test.ts # packages/provider-limrun/src/interaction-operations.ts # packages/provider-webdriver/src/platform-runtime.ts # src/core/command-descriptor/registry.ts # src/core/dispatch.ts # src/platforms/apple/plugin.ts
|
Still not ready at |
…/tv-remote/keyboard Following main's #1969 (facade granularization), give each of this branch's five new contract modules their own package.json entry subpath and move every value-importer (owner runtime packages, the daemon binders, and their tests) off the wide @agent-device/contracts/platform facade onto the specific module that owns the symbol — the same convention #1969 established for the rest of the vocabulary. Keeps this migration's files out of the contracts-entry-closure gate and out of the eager-evaluation cost #1969 measured for the daemon's permanent hubs (registry.ts, dispatch.ts).
…mission; drop restated types
Addresses the review's finding 1 (seven per-owner copies of the same
"fact-keyed table of interactor binders" pattern) by extracting
bindAdmittedLocalInteractorOperations/bindAdmittedProviderInteractorOperations
into packages/contracts/src/interactor-operation-catalog.ts. Each owner now
requests the subset of back/home/setOrientation/tvRemote/keyboard{Status,
Dismiss,Enter} it admits, instead of hand-writing
`facts.operations.<key>.available ? bind…(resolver) : {}` per operation.
Applied across all seven call sites (apple, android, harmonyos, vega, linux,
webdriver, limrun) and collapsed limrun's two separate bind functions
(navigation, keyboard) into one shared call.
Finding 3 (resolveBoundKeyboardRuntime copy-pastes admit-then-wrap three
times): extracted a local admitKeyboardAction<...> helper mirroring
resolveBoundGenericRuntime's admit-then-defer shape, so the three action
branches (status/dismiss/enter) share one admission path.
Finding 6 (execute* helpers hand-restate a contract that can drift): back/
home/orientation/tv-remote/keyboard's execute functions are now typed off
`BoundDeviceRuntime<typeof xRuntimeUse>` (derived from the actual bind-use
value) instead of a hand-written `Readonly<{ operations: Readonly<{...}> }>`
shape. Also fixed provider-limrun's `RuntimeOperationUnavailability |
{ available: true }` restating RuntimeOperationFact by hand — folded away
entirely once the bind functions it typed were removed.
Finding 7 (naming/placement): platform-apple/runtime.ts's misleadingly-named
`captureOperations` bucket (held deployment/network/recording/find, not just
capture) collapsed into one flat `operations` object now that the navigation
bucket is a single function call instead of six ternaries.
Exported RuntimeAdmissionRequest from runtime-admission.ts (needed by the new
keyboard admission helper). Added packages/contracts/src/
interactor-operation-catalog.test.ts for the new shared binder table.
pnpm typecheck, check:fallow, check:layering, and the full unit-core suite
(1010 files / 7513 tests, one known contention-flake excluded) are green.
…tch pair Addresses the review's finding 5: GENERIC_MUTATING_LINUX_DEVICE_COMMAND_TRAITS bundled two orthogonal things (daemon/recording traits, and the legacy capability+dispatch pair migration strips), forcing every migrated descriptor to hand-expand the constant minus two fields plus an explanatory comment. Split into GENERIC_MUTATING_COMMAND_TRAITS (the shared daemon/recording traits) and LEGACY_LINUX_DEVICE_EXECUTION (the dispatch/capability pair). back/home/orientation/tv-remote (this migration) and focus (an earlier one, same pattern, previously a stale reference to the retired constant name) now spread the trait constant directly instead of hand-expanding it; the still-legacy `scroll` descriptor spreads both pieces, equivalent to the retired constant. pnpm typecheck, check:fallow, check:layering, and the registry/daemon test suites are green.
…e-way duplication Finding 3's second half: the three bindKeyboardX functions and six bindLocal/ProviderKeyboardXInteractor entry points differed only by method name and label string. Replaced with one generic bindKeyboardAction<Key> dispatching off the operation key (interactor[key], resolved from a small label table) plus two shared local/provider dispatch helpers the six named exports each call with their own key — collapsing three copies of the bind logic into one and six near-duplicate entry-point bodies into one line each, while keeping every exported name and type signature unchanged. pnpm typecheck, check:fallow, check:layering, and pnpm check:affected --run are green.
|
The latest commits correctly remove several duplicated binder/admission patterns, but three owning-structure blockers remain:
This is still an escalated change (~1,946 net production-ish lines, +12.8 kB unpacked). Update the stale PR description to current scope and itemize justified growth plus why the remaining smaller designs were rejected after the known deletions above. Exact-head checks currently have no failures, but Coverage and Android/iOS smoke were still running when reviewed. |
1 similar comment
|
The latest commits correctly remove several duplicated binder/admission patterns, but three owning-structure blockers remain:
This is still an escalated change (~1,946 net production-ish lines, +12.8 kB unpacked). Update the stale PR description to current scope and itemize justified growth plus why the remaining smaller designs were rejected after the known deletions above. Exact-head checks currently have no failures, but Coverage and Android/iOS smoke were still running when reviewed. |
…trategy Addresses the review's finding 2: handleKeyboardCommand re-implemented runSessionOrSelectorDispatch's orchestration step for step (session/selector guard, device resolve, ref-frame expiry, record) instead of reusing it, because the shared function had no seam for keyboard's bind-and-execute admission — only the legacy requireCommandSupported + dispatchCommand path. That left the shared orchestrator with one caller instead of two, and set a precedent that would fork a new copy for each of the 28 remaining session-route migrations. Gave runSessionOrSelectorDispatch an `execute` parameter: the orchestration (guard, resolve device, admit-then-execute, expire ref frame if mutating, derive and record next session) stays in one place, and callers supply their own admission/execution strategy. Extracted `legacySessionDispatchExecute` for the still-legacy capability-gate-then-dispatchCommand shape `handleTriggerAppEventCommand` (the remaining legacy caller) now passes explicitly, and `keyboardSessionExecute` for keyboard's bind-and-execute shape. Deleted the now-fully-redundant `executeBoundKeyboardCommand` — its result recording duplicated what the shared orchestrator's tail already does. pnpm typecheck, check:fallow, check:layering, the full daemon test suite (321 files / 2271 tests), and pnpm check:affected --run are green.
|
Fixed 6 of 7 findings, pushed across four commits ( Finding 1 (seven copies of the fact-keyed binder table) — fixed. Extracted Finding 3 (copy-pasted admit-then-wrap) — fixed, both halves.
Finding 2 ( Finding 5 (hand-expanded Finding 6 (restated contract types) — fixed both instances.
Finding 7 (naming/placement) — fixed both bullets. Each commit is Finding 4 ( |
…ssResult by owner app-log-runtime.ts was still 589 lines after the shared-abstraction fixes; moves fact assembly (limrunAppLogFacts/limrunAppLogRecoveryFacts/limrunLifecycleFacts/deploymentOptions) to a new facts-runtime.ts and the shared device-identity predicate to device.ts, the leaf both files already depend on. app-log-runtime.ts is now 336 lines. KeyboardDismissResult was an 11-field optional bag with executeKeyboardDismiss separately re-deriving platform from the device and projecting subsets by hand. Each owner (android, apple, harmonyos) now tags its own result with a `kind` discriminant, so an owner can only ever produce its own shape, and the daemon derives the wire `platform` label from `kind` instead of guessing from the device a second time. Wire output is unchanged.
|
All three owning-structure blockers are now fixed, and the PR description is updated. 1. Note 2.
Wire output is unchanged: 3. PR description — updated. Restated "Shared helper" as "Shared abstractions" covering all four abstractions these review rounds produced (not just Verified at |
Wave 5 unit for #1739 (ADR 0019) — the five remaining generic-route leaves:
back,home,orientation,tv-remote,keyboard.What changed
One execution path per command. Each moves off
dispatchKnownCommand/Interactorlegacydispatch onto exact-owner runtime facts, admitted and bound exactly once per handler (ADR 0019
§9).
back/home/orientation/tv-remotestaydaemon.route: 'generic';keyboardstaysdaemon.route: 'session'since it can run sessionless, and uses R35's action-selected single-bindpattern —
status/dismiss/entereach resolve and bind their ownRuntimeUserather thanadmitting all three together.
Facts replace the retired admission, restated per owner from the deleted closures:
back/home: no apple-family closure ever gatedbackbeyond device kind (tvOS's Menu-buttonnavigation included);
homeis unavailable only on macOS (an already-running app, nospringboard). Android/HarmonyOS ride the same touch gate as focus/type; Linux/Web/Vega restate
their retired per-platform buckets.
orientation: unavailable on tvOS and macOS (no device orientation there), otherwise mirrorsthe retired
supportsOrientationclosure per Apple OS.tv-remote: available only on tvOS and a real Android TV target (device.target === 'tv') —the mobile-vs-TV gate that used to live in the plugin closure now lives in the owner's fact.
keyboard:statusis Android-only (no live IME read exists elsewhere — the retired in-handlerhint is preserved byte-for-byte);
dismiss/enterare cross-platform wherever the interactorreaches a foreground app.
Registry: all five descriptors flip to
device-runtime.HARMONYOS_SUPPORTED_COMMANDSdropsback/home/keyboard; the apple plugin's
supportsKeyboard/supportsOrientation/supportsTvRemoteclosures and Vega's
VEGA_VVD_ONLY_COMMANDS/target-gating closures are deleted. R42–R46 are thefive new cutover rows.
Shared abstractions (the last several of these landed in response to review — see below):
resolveBoundGenericRuntime(src/daemon/runtime-admission.ts) collapses the admit-then-wrapboilerplate duplicated across back/home/orientation/tv-remote/focus into one call.
bindAdmittedLocalInteractorOperations/bindAdmittedProviderInteractorOperations(
packages/contracts/src/interactor-operation-catalog.ts) replace the seven owner-packagecopies of "fact-keyed table of interactor binders" with one shared dispatch table; each owner
names the operation subset it admits.
runSessionOrSelectorDispatch(src/daemon/handlers/session.ts) now takes anexecutestrategy parameter, so
keyboard's bind-and-execute admission shares the session/selector-routeorchestration with the still-legacy
dispatchCommandpath instead of forking its own copy.packages/contracts/src/keyboard-runtime.ts's three near-identical bind functions and sixnear-identical entry points collapsed into one generic dispatcher plus two thin call sites.
Review-driven architecture changes
A deep structural review flagged real duplication and two over-budget files this migration grew
further. Fixed, in commits after the initial live-evidence push:
interactor-operation-catalog.tsdispatch table (above)handleKeyboardCommandforkedrunSessionOrSelectorDispatch's orchestrationexecutestrategy; deleted the forkkeyboard-runtime.ts(daemon + contracts) copy-pasted admit-then-wrap three timesGENERIC_MUTATING_LINUX_DEVICE_COMMAND_TRAITSbundled shared traits with the legacy dispatch pair, forcing hand-expansion on every migrated descriptorGENERIC_MUTATING_COMMAND_TRAITS+LEGACY_LINUX_DEVICE_EXECUTIONexecute*helpers hand-restated the bound-runtime operations shapeBoundDeviceRuntime<typeof xRuntimeUse>insteadplatform-apple/runtime.ts'scaptureOperationsbucket named after only part of its contentsoperationsobject once the catalog removed the complexity pressureprovider-limrun/app-log-runtime.tsretained new fact/bind assembly past the 500-line budgetfacts-runtime.ts(fact assembly + lifecycle facts) and moved the shared device-identity predicate todevice.ts, the existing leaf both files needGrowth accounting, current head vs.
main, after the fixes above (no remaining hand-expansionor forked orchestration):
packages/platform-apple/src/runtime.tsnavigation/runtime.ts(new, 141 lines)packages/provider-limrun/src/app-log-runtime.tsfacts-runtime.ts(new, 249 lines)packages/provider-limrun/src/interaction-operations.tspackages/contracts/src/keyboard-runtime.tspackages/contracts/src/interactor-operation-catalog.tssrc/daemon/handlers/session.tsOne item from the deep review is intentionally not done in this PR: redesigning
KeyboardDismissResultas a discriminated union. It's the right fix, but it touches the daemon's wire response shape, and
daemon-wire-compat(the gate that would catch a mistake there) is GitHub-authoritative, notrunnable locally — see the review thread for the concrete design and the reasoning for deferring
it as a follow-up.
Test evidence, with the mutants run up front
Contract binder tests mirror the established mutant-style shape (local binding drives the
interactor with the right args, provider binding drives its own interactor, provider binding
fails closed with no interactor via
UNSUPPORTED_OPERATION/provider-runtime-interactor-missing,an already-cancelled request never resolves an interactor). Representative mutants planted and
killed, source restored byte-identical after each:
back-runtime.tsawait interactor.back(input.mode)home-runtime.tsawait interactor.home()orientation-runtime.ts'portrait'instead ofinput.rotationtv-remote-runtime.tsinput.durationMs, passedundefinedkeyboard-runtime.tsbindKeyboardDismisstointeractor.keyboardEnterinstead ofinteractor.keyboardDismisskeyboard-runtime.tsadditionally covers therequireKeyboardMethodruntime-contract-error path(
COMMAND_FAILED/interactor-method-missing) when an interactor's fact admits an operation itsobject doesn't implement.
Owner fact-cell tests were added across all 8 owner suites —
platform-android,platform-harmonyos, andplatform-vegahad zero coverage of the new operations before this PRand gained full
test.eachfact-cell coverage;platform-apple,platform-linux,platform-web,provider-webdriver, andprovider-limrun(including a new standaloneinteraction-operations.test.tsfor the pure Android/iOS navigation-fact module) were extended.Added
interactor-operation-catalog.test.tsandfacts-runtime.test.tsfor the two new sharedmodules the review-driven fixes introduced.
Six smoke-coverage integration oracles (android/ios-simulator/macos/tvos/web/linux) had their own
independent capability-catalog assertions for these five commands; all were repointed at the new
fact-cell evidence and reclassified
capability-denial→command-contractwhere the command isnow fact-owned rather than catalog-owned, with classification-summary counts updated to match.
Nine pre-existing daemon/capability unit tests broke on the retirement and were fixed: the direct
capability-matrix oracle (
capabilities.test.ts), the descriptor parity oracle(
command-descriptor/parity.test.ts), and — most notably — tworequest-router-replay-scope.test.tstests whose
.adreplay fixtures usedhome/backas throwaway stand-in commands for testingunrelated router mechanics (cost tracking, response-level views, lock policy). Those fixtures
called into real interactor code against a fake iOS simulator device once
home/backstoppedrouting through the mocked
dispatchCommand, spawning realxcodebuildprocesses and timing outat 5s. Swapped the representative command to
app-switcher/scroll, which still route throughlegacy dispatch and don't touch this migration.
A CI-only Coverage failure (
orientation-runtime.test.tsspawning a real, unmockedadbprocessthrough
androidBlockingDialogGuardon a host with no Android SDK) was root-caused and fixed bystubbing the same guard seam
request-router-android-modal.test.tsalready uses, keeping theAndroid fixture rather than swapping it to Apple to dodge the guard.
Live evidence
Real devices at this head — iPhone 17 Pro simulator, Pixel_7_review AVD, a fresh Android TV AVD
(
Television_4K,target=tv), a fresh tvOS 26.2 simulator, and a real Vega Virtual Device:backBack(Menu-equivalent)Back(Menu-equivalent)Back— real remote navigationhomeHomeHomeHomeHome— real remote navigationorientationunsupported-platform-leafwith no hint text)orientation is not supported on Vega OS.tv-remotetv-remote is supported only on Android TV targets.select/right— genuinely admitted (target=tv)select/down— genuinely admittedselect— real remote navigationkeyboard statuskeyboard status/get is currently supported only on Android; use keyboard dismiss or enter on iOSvisible:false(idle) →visible:true(focused) — real IME statekeyboard is not supported on Vega OS.keyboard dismissdismissed:trueafter 2 attempts, verifiedvisible:falsekeyboard enterKeyboard enter pressedKeyboard enter pressedAll sessions closed; the tvOS simulator deleted, both Android AVDs shut down, and the Vega VVD
stopped after the run.
Gate
pnpm check:affected --run: all runnable checks passed on every pushed head, including after thereview-driven architecture changes above.
pnpm check:layeringrecognizes R42–R46 among themigrated commands with singular execution proven per operation.
pnpm check:fallow: zerodead-code/complexity/duplication findings at current head — the duplication findings the review
caught (the seven-owner binder ternary, the three-way keyboard admit-then-wrap, the forked session
orchestration) are fixed by extraction, not suppressed.
103 files, +5511/−1083 at current head.