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
Adds agent-device connect limrun backed by a direct Limrun provider runtime for iOS and Android.
The shared provider runtime remains limited to lease lifecycle, inventory, interactor dispatch, install hooks, optional port reverse hooks, and shutdown. Limrun API calls, assets, ADB tunnel handling, Android snapshot/helper reuse, and iOS tree mapping remain local to src/providers/limrun.
Android provider interactor composition now lives in core so future direct Android providers can reuse the same ADB-backed interaction stack without importing platform internals. The runtime tags Limrun requests and instance metadata with agent-device identity.
Validation
pnpm check:affected --base origin/main --run
pnpm check:layering
pnpm exec vitest run src/__tests__/limrun-runtime.test.ts src/platforms/android/__tests__/app-lifecycle-open.test.ts
Live Limrun Android Gesture Lab and helper-backed snapshot verification completed before this rebase; iOS and Android direct sessions were also exercised.
Overall this is a well-shaped PR. The ProviderDeviceRuntime seam is the right home for this, splitting device.ts/snapshot.ts out of the runtime was the right instinct, and hoisting the withAndroidAdbProvider Proxy from the Limrun runtime into createAndroidInteractor (core/interactors/android.ts) is exactly the canonical-layer move — the ADB-provider scope now composes generically for any future direct Android provider without importing platform internals. Good.
That said, there are a few structural issues I'd want addressed before merge. Two are load-bearing.
1. configureDirectPortReverse is inert in production — a de-facto test-only seam
react-devtools.ts grows an option (configureDirectPortReverse?, L30), a gate (shouldConfigureDirectReverse, L159), and a call site (L180). But the only caller of runReactDevtoolsCommand — cli.ts:212 — never passes configureDirectPortReverse. So the gate's options.configureDirectPortReverse !== undefined clause (L169) is always false in production, the branch never fires, and the only thing that exercises it is the unit test that injects a vi.fn().
This is precisely the pattern the No test-only DI seams guard exists to catch; it passed here only because the seam is an optional callback rather than a flagged shape. Functionally it's dead branching that reads as "direct RDT port-reverse is wired" when it isn't.
Please either wire it at the cli.ts call site (the daemon already exposes configureProviderPortReverse, so the callback has a real home) or drop it from this PR until the consumer exists. Shipping the gate + option + branch with no production producer is debt, and it's the kind of thing that quietly becomes permanent.
2. Android port-reverse: bespoke ownership tracking duplicates the canonical helpers, and stacks on top of them
runtime.ts hand-rolls a full port-reverse ownership/idempotency/conflict layer:
All of this already exists canonically in platforms/android/adb-executor.ts: createExecAndroidPortReverseProvider (owner tracking) + createAndroidPortReverseManager (idempotency + "already owned by" conflict). The Limrun exec (runLimrunAndroidAdb) already runs adb -s <serial> …, which is exactly the executor those helpers expect.
Worse than plain duplication: because the runtime defines its ownreverse provider, the app-lifecycle path now double-books the same ports. ensureAndroidLocalhostReverse (app-lifecycle.ts:278) does createAndroidPortReverseManager(resolveAndroidAdbProvider(device)) — inside a Limrun scope resolveAndroidAdbProvider returns the Limrun provider, so the canonical manager's active map is layered directly on top of the bespoke reversedPorts map. Two trackers, same ports, same idempotency/conflict logic implemented twice.
Code-judo move: build one canonical reverse provider per session and back both entry points with it. Roughly:
// once, in createAndroidSession (closes over session, lazily starts the tunnel via runLimrunAndroidAdb):session.reverse=createExecAndroidPortReverseProvider((args,options)=>runLimrunAndroidAdb(session,args,options),);
Then createLimrunAndroidAdbProvider sets reverse: session.reverse, configurePortReverse/removePortReverse (L167/L179) delegate to session.reverse.ensure/remove with tcp:${port} endpoints, and cleanupAndroidAdbTunnel uses list()/removeAllOwned() instead of iterating reversedPorts. That deletes ~90 lines (the four helpers + the map + most of tcpEndpointPort) and removes the stacked double-tracking. The per-session persistence you (correctly) need is preserved by storing the single provider instance on the session — which is the actual reason the bespoke map exists, and the one thing the ephemeral per-call managers don't give you.
3. inferAndroidAppName collides with — and shadows — the canonical export
runtime.ts:763 defines a private inferAndroidAppName(packageName): string | undefined (last-segment heuristic). platforms/android/app-lifecycle.ts:176 already exportsinferAndroidAppName(packageName): string, and it's the more capable version (filters com/android/google/… noise tokens). Reuse the canonical one; a same-named, weaker private copy is the textbook bespoke-duplicate smell and will drift.
Under the 1k line, so not a hard blocker — but this new file already spans three concerns that want to be separate, and you've established the split pattern with device.ts/snapshot.ts:
the LimrunRuntime class + lease/inventory/install dispatch/shutdown
Splitting into runtime.ts + ios.ts + android.ts drops the orchestration file well under ~350 lines and stops the iOS/Android tagged-union from forcing both platforms to share a module. Worth doing while the code is fresh.
Minor
readLimrunLeaseIdFromInventoryRequest (device.ts:42) is an identity wrapper — it returns request.leaseId. Inline it at the one call site; the named helper adds indirection without buying clarity.
parseLimrunDeviceId (device.ts:32) returns null while its siblings (platformForLimrunLeaseBackend, getSessionForDevice, etc.) return undefined. Pick one absent-value convention.
ensurePersistentAndroidAdbSerial (L712) isn't concurrency-safe: two parallel adb calls in one session (the Android platform code does fan out) can both see !adbTunnel and each call startAdbTunnel(), leaking a tunnel. Memoize the in-flight promise on the session. The AndroidAdbExecutor contract explicitly says implementations must be safe to call concurrently for one request.
None of these are correctness-critical to the happy path, and the core architecture is sound — but #1 and #2 are real structural debt (inert branch; duplicated + stacked port-reverse), and both have a clean path to deleting code rather than rearranging it. I'd resolve those two before merge.
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
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.
Summary
Adds
agent-device connect limrunbacked by a direct Limrun provider runtime for iOS and Android.The shared provider runtime remains limited to lease lifecycle, inventory, interactor dispatch, install hooks, optional port reverse hooks, and shutdown. Limrun API calls, assets, ADB tunnel handling, Android snapshot/helper reuse, and iOS tree mapping remain local to
src/providers/limrun.Android provider interactor composition now lives in core so future direct Android providers can reuse the same ADB-backed interaction stack without importing platform internals. The runtime tags Limrun requests and instance metadata with agent-device identity.
Validation
pnpm check:affected --base origin/main --runpnpm check:layeringpnpm exec vitest run src/__tests__/limrun-runtime.test.ts src/platforms/android/__tests__/app-lifecycle-open.test.ts