Skip to content

[agent-cli] navigate judges attachment on the device it opens - #200

Merged
vonovak merged 17 commits into
vonovak/fix-reload-skipped-appsfrom
vonovak/fix-navigate-cloud-attach
Oct 30, 2026
Merged

vonovak merged 17 commits into
vonovak/fix-reload-skipped-appsfrom
vonovak/fix-navigate-cloud-attach

Conversation

@vonovak

@vonovak vonovak commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Every runtime command acts on the device bound to this worktree, and nothing else attached to the same dev server, and says which device it acted on.

This PR makes navigate decide "attached" from the device it opens.

Stack: #202 → #203 → #204 → #206 → #217 → #218 → #207 → #200 (this is 8/8).

Problem

navigate counted the attached apps of every platform. N3-25 (also DF22): after dev --android --eas, with only a local iPhone 17 Pro app on port 8081, navigate "/street/…" with no flag counted the iOS app as attached. It skipped the launcher, opened only the route link, and exited 22 after 120 s. On Android, the stuck-app recovery could also stop an app that another device ran on the same dev server.

Fix

navigate validates the route, resolves the device, and then reads the attached apps of that device's scope to decide the app kind, the route URL and the launcher. One reader serves the launcher wait and the route wait. It reads the device facts again only for a changed list, within the budget of the wait that polls. So a cloud app that attaches after the accepted Expo Go dialog counts. When an Android twin of the bound device is attached, navigate reads adb reverse before the launcher decision and again after this run's own adb reverse. When nothing attached on Android, the recovery stops --app-id, else the app config's app, else Expo Go. smoke hands its device to the route open, so the open and the waits are about one device. --print-url reads the scope as navigate does, adb reverse included, once the dev server lists an app. The recovery stops Expo Go when this run opened it, and says the app was stopped only when pidof saw it running.

The first commit is a pure move of route URL resolution to src/navigate/routeResolution.ts. The second commit holds the behavior.

Red tests: navigate-target-selection-test "no flag, bound Android emulator, only an iOS app and another model's Android app attached"; navigateAsync-test "asks adb for the device list once while the attach wait polls an unchanged list"; e2e cloud-simulator-test "counts the app that attaches after the dialog is accepted"; navigate-target-selection-test adds three Android twin rows, one of them "no flag, bound Android emulator beside a twin, neither reverses the port before this run".

Live check: the live-cloud iOS suite passed 17 of 17 at the stack top on an EAS session: navigate / --eas exit 0, attached after 3 ms. Plain main gives the same results.

Watch: after a wait's budget ends, its later polls keep the last device facts.

🤖 Generated with Claude Code

@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from 2b80c47 to bd3ba08 Compare October 23, 2026 22:22
@vonovak
vonovak marked this pull request as ready for review October 24, 2026 03:41
@vonovak
vonovak requested a review from Kudo October 24, 2026 03:41
@vonovak
vonovak marked this pull request as draft October 24, 2026 05:08
@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from bd3ba08 to b7deaa9 Compare October 24, 2026 20:50
@vonovak
vonovak changed the base branch from main to vonovak/fix-reload-skipped-apps October 24, 2026 20:50
@vonovak
vonovak added this pull request to stack #210 October 26, 2026 09:54
@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from b7deaa9 to b06c81f Compare October 26, 2026 17:44
@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from b06c81f to c501329 Compare October 26, 2026 18:24
@vonovak
vonovak removed this pull request from stack #210 October 27, 2026 17:52
@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from c501329 to a62ff9e Compare October 27, 2026 17:53
@vonovak
vonovak added this pull request to stack #212 October 27, 2026 17:53
@vonovak vonovak changed the title [agent-cli] navigate without a platform flag judges the attached app on the device it opens (llp/0005 §Pointing an app at this dev server) [agent-cli] navigate judges attachment on the device it opens Oct 27, 2026
@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from a62ff9e to 364b0d2 Compare October 27, 2026 19:38
@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from 364b0d2 to 7de42ed Compare October 27, 2026 21:29
@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from 7de42ed to 506d819 Compare October 27, 2026 21:52
@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from 2cdfae9 to 298c5e2 Compare October 29, 2026 14:38
@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from 298c5e2 to db9826c Compare October 29, 2026 15:01
@vonovak
vonovak marked this pull request as ready for review October 29, 2026 15:16
@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from db9826c to 6023a0f Compare October 29, 2026 18:46
@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from 6023a0f to fab9a14 Compare October 29, 2026 19:26
const { appId } = resolveAppId({
platform: device.platform,
appIdOverride: options.appId,
targetAppIds: [],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recovery always passes targetAppIds: [], so resolveAppId skips every app on the dev server and stops android.package when the project has one.

That is the wrong process when this run opened Expo Go. A stuck Expo Go on this device is not in /json/list, but another Android device's Expo Go on the same server is, and its id is host.exp.exponent. adb -s <this serial> shell am force-stop of that id stops only this device. The old resolveStuckAppIdAsync used that id. This force-stops android.package instead, which exits 0 even when that package is not running, then opens the link again into the same stuck Expo Go.

android.package is normal on a managed app that still runs in Expo Go, so the "else Expo Go" branch almost never runs. With an empty target list the old and new paths already agreed. The regression is this cross-device Expo Go id: it is the same id on every Android device, and it was a correct stop target. Keep ignoring other devices' development-build ids. When resolved.isExpoGo is set, stop EXPO_GO_APP_ID[platform] rather than the config package.

@vonovak vonovak Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f2269fc

` fs.appendFileSync(reversed, JSON.stringify([args[1], Number(args[3].slice(4))]) + '\\n');`,
` process.exit(0);`,
`}`,
`if (args[2] === 'shell' && args[3] === 'am' && args[4] === 'start' && args.includes('-d')) {`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accepts am start -d, but the new recovery rows also run pidof and am force-stop. Those still fall through to the unexpected exit 2 below. stopAppOnDeviceAsync treats a non-zero force-stop as failure, so recovered stays false and the second open never happens.

The new tests still pass. They only check that a force-stop argv was recorded, not that stderr says the app was stopped and the link opened again. The unit test that drove a successful force-stop was removed. Accept pidof (exit 1, empty stdout) and am force-stop (exit 0), and assert that recovery sentence on the exit-22 rows.

@vonovak vonovak Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 67e7eff

@vonovak
vonovak force-pushed the vonovak/fix-navigate-cloud-attach branch from fab9a14 to 7b1fece Compare October 29, 2026 19:34
vonovak and others added 17 commits October 30, 2026 10:35
…routeResolution

A pure move, with no change in behavior. resolveRouteUrlAsync,
currentTunnelHost, isCallerNamedDevServer and their private helpers move
to src/navigate/routeResolution.ts. openRoute.ts imports them and
re-exports the three public functions, so no caller changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
navigate validates the route, resolves the device, and then reads the
attached apps of that device's scope to decide the app kind, the route
URL and the launcher. The same reader serves the launcher wait and the
route wait. It builds the scope from the device without probing, reads
the device facts once with the first list, and asks the device tools
again only for a changed list, within the budget of the wait that
polls. So an app that attaches after the accepted Expo Go dialog or the
Android recovery counts. When another attached Android device has the
bound device's model, it asks adb reverse before the launcher decision
and again after this run's own reverse. An app that cannot
be placed on the device neither counts as attached nor holds back the
launcher. --print-url resolves no device.

When nothing attached on Android, the stuck-app recovery stops
--app-id, else the app config's app, else Expo Go. This device has no
app attached at that point, so an app another device runs on the same
dev server is never the one stopped.

smoke hands its device choice to openRouteAsync, so the open and the
waits after it are about one device. A device lookup that failed is
asked again at the route open, and throws its own refusal there.
OpenRouteOptions.device is a NavigateDevice; the open extends no lease
for a device it is given.

Route URL resolution owns its options (RouteUrlOptions) and its result
(ResolvedRoute); OpenRouteOptions extends the options, and callers of
resolveRouteUrlAsync import it from navigate/routeResolution.

The built-command table in navigate-target-selection-test covers local
devices, three Android twin rows, cloud fallback with and without a
platform flag, launcher ordering, and an empty first list followed only
by a local app. cloud-simulator-test counts an app that attaches after
the dialog is accepted. navigateAsync-test pins the recovery's app id and one
adb device list per unchanged list. The rules are in llp/0005 §Pointing
an app at this dev server and §adb reverse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The navigateAsync unit cases for Android become rows and asserts in navigate-target-selection-test:
the launcher goes to the main activity before the route link, adb reverse comes first, a run whose
app never attaches exits 22 and force-stops only the configured app, and an attached app gets only
the route link. Expo Go on Android gets only the route link, in navigate-test. An explicit
--ios --eas --app-id row replaces the cloud-simulator case it duplicated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cases that pin them

The force-stop rule, the EAS Expo Go row and the session development-build sentence cite the
navigate-target-selection and cloud-simulator e2e cases that replaced their unit tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e classifier now takes it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the worktree is bound to an EAS session (llp/0005 §navigate)

resolveRouteUrlAsync now reads the worktree's binding and the caller's --eas, so
--print-url, dev --eas and runtime:reload --eas decide Expo Go on the same scope
as navigate. An unreadable binding refuses them, like every scoped command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pens the link again and sees it attach (llp/0005 §navigate)

The stub adb now answers shell pidof and shell am force-stop, so the recovery's
success path runs in e2e: attachRecovered is true and the app counts as attached.
The rows where no app ever attaches now see the second open of the route link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… no env var for it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…attaches during the attach wait (llp/0035 §Readers)

The wait asks adb reverse again when the new list shows twins, so the twin does not leave the own app undetermined until the attach timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…run opened Expo Go (llp/0005 §navigate)

The stub adb already accepts pidof and am force-stop; the exit-22 rows now
assert the recovery sentence in stderr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tools on

AGENT_CLI_NO_DEVICE=1 now skips the EAS binding unless the run passes --eas, so the case sets it to 0 like the other bound-device cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd the wait-retry test passes its deadline per read

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n through adb reverse, as navigate does (llp/0005 §navigate)

The URL resolver passed no dev server URL, so the reverse question was never asked and the Expo Go app on the bound emulator stayed undetermined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e comes before the route link

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g (llp/0005 §navigate)

When pidof sees no app, the attach recovery still opens the link again, but attachRecovered stays false and the failure text says nothing was stopped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…binding when the dev server lists no app (llp/0005 §navigate)

The scope is read only once the dev server lists an app, so a corrupt binding file no longer refuses a URL that does not depend on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… macOS only, where a local iOS binding is read

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants