Repository navigation
[agent-cli] runtime:reload and smoke send no broadcast while the run skips an app - #207
Merged
vonovak merged 27 commits intoOct 30, 2026
Merged
Conversation
This was referenced Oct 24, 2026
Merged
vonovak
marked this pull request as ready for review
October 24, 2026 03:41
vonovak
marked this pull request as draft
October 24, 2026 05:08
vonovak
added a commit
that referenced
this pull request
Oct 24, 2026
…with or without a platform flag (llp/0005 §Pointing an app at this dev server) Problem (N3-25): right after `dev --ios --eas`, a `navigate` with no platform flag waited 120 s and exited 22. The run decided "an app is attached" from the platform flag, before the device was known. With no flag it counted the debugger targets of every platform, so a local simulator's app on the same Metro counted as attached, and the launcher open for the session did not happen. A no-flag navigate to a local Android emulator with only an iOS app attached made the same mistake. `navigate --ios` that fell back to an EAS Simulator iOS session without `--eas` had the reverse fault: the flag scope cannot place the session's app, so the run reopened an attached app through the launcher. Change: `openRouteAsync` builds one attach waiter from the scope of the device it resolved, and asks it whether an app is attached before it decides on the launcher open. The same waiter serves the launcher wait and the route-link wait, and reads the device facts once, at its first probe. The route resolver no longer reports an attached flag. llp/0005 states the rule in the section on pointing an app at this dev server. This commit sits on the batch-2 stack (#202-#207, RunScope in src/runtime/runScope.ts); its base is vonovak/fix-reload-skipped-apps. Red before: navigateAsync on a development build that is not loaded > opens the launcher URL first, when no platform is named and only an iOS app is attached; navigateAsync on an EAS Simulator session > opens, with no flag, an Android session, only an iOS app attached; navigateAsync on an EAS Simulator session > opens, with --ios without --eas, an iOS session, its app attached Guard: navigateAsync on an EAS Simulator session > opens, with no flag, an Android session, its app attached; navigateAsync on an EAS Simulator session > opens, with no flag, an iOS session, its app attached Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
force-pushed
the
vonovak/fix-reload-skipped-apps
branch
from
October 26, 2026 17:44
d1b95e1 to
9b2210d
Compare
vonovak
added a commit
that referenced
this pull request
Oct 26, 2026
…with or without a platform flag (llp/0005 §Pointing an app at this dev server) Problem (N3-25): right after `dev --ios --eas`, a `navigate` with no platform flag waited 120 s and exited 22. The run decided "an app is attached" from the platform flag, before the device was known. With no flag it counted the debugger targets of every platform, so a local simulator's app on the same Metro counted as attached, and the launcher open for the session did not happen. A no-flag navigate to a local Android emulator with only an iOS app attached made the same mistake. `navigate --ios` that fell back to an EAS Simulator iOS session without `--eas` had the reverse fault: the flag scope cannot place the session's app, so the run reopened an attached app through the launcher. Change: `openRouteAsync` builds one attach waiter from the scope of the device it resolved, and asks it whether an app is attached before it decides on the launcher open. The same waiter serves the launcher wait and the route-link wait, and reads the device facts once, at its first probe. The route resolver no longer reports an attached flag. llp/0005 states the rule in the section on pointing an app at this dev server. This commit sits on the batch-2 stack (#202-#207, RunScope in src/runtime/runScope.ts); its base is vonovak/fix-reload-skipped-apps. Red before: navigateAsync on a development build that is not loaded > opens the launcher URL first, when no platform is named and only an iOS app is attached; navigateAsync on an EAS Simulator session > opens, with no flag, an Android session, only an iOS app attached; navigateAsync on an EAS Simulator session > opens, with --ios without --eas, an iOS session, its app attached Guard: navigateAsync on an EAS Simulator session > opens, with no flag, an Android session, its app attached; navigateAsync on an EAS Simulator session > opens, with no flag, an iOS session, its app attached Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
force-pushed
the
vonovak/fix-reload-skipped-apps
branch
from
October 26, 2026 18:24
9b2210d to
c9ea36d
Compare
vonovak
added a commit
that referenced
this pull request
Oct 26, 2026
…with or without a platform flag (llp/0005 §Pointing an app at this dev server) Problem (N3-25): right after `dev --ios --eas`, a `navigate` with no platform flag waited 120 s and exited 22. The run decided "an app is attached" from the platform flag, before the device was known. With no flag it counted the debugger targets of every platform, so a local simulator's app on the same Metro counted as attached, and the launcher open for the session did not happen. A no-flag navigate to a local Android emulator with only an iOS app attached made the same mistake. `navigate --ios` that fell back to an EAS Simulator iOS session without `--eas` had the reverse fault: the flag scope cannot place the session's app, so the run reopened an attached app through the launcher. Change: `openRouteAsync` builds one attach waiter from the scope of the device it resolved, and asks it whether an app is attached before it decides on the launcher open. The same waiter serves the launcher wait and the route-link wait, and reads the device facts once, at its first probe. The route resolver no longer reports an attached flag. llp/0005 states the rule in the section on pointing an app at this dev server. This commit sits on the batch-2 stack (#202-#207, RunScope in src/runtime/runScope.ts); its base is vonovak/fix-reload-skipped-apps. Red before: navigateAsync on a development build that is not loaded > opens the launcher URL first, when no platform is named and only an iOS app is attached; navigateAsync on an EAS Simulator session > opens, with no flag, an Android session, only an iOS app attached; navigateAsync on an EAS Simulator session > opens, with --ios without --eas, an iOS session, its app attached Guard: navigateAsync on an EAS Simulator session > opens, with no flag, an Android session, its app attached; navigateAsync on an EAS Simulator session > opens, with no flag, an iOS session, its app attached Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
force-pushed
the
vonovak/fix-reload-skipped-apps
branch
from
October 27, 2026 17:53
c9ea36d to
4e65852
Compare
vonovak
force-pushed
the
vonovak/fix-reload-skipped-apps
branch
from
October 27, 2026 19:38
4e65852 to
746c2f0
Compare
vonovak
force-pushed
the
vonovak/fix-reload-skipped-apps
branch
from
October 27, 2026 21:29
746c2f0 to
e4fa28a
Compare
vonovak
force-pushed
the
vonovak/fix-reload-skipped-apps
branch
from
October 27, 2026 21:52
e4fa28a to
695047e
Compare
vonovak
force-pushed
the
vonovak/fix-reload-skipped-apps
branch
from
October 27, 2026 22:01
695047e to
b8807cd
Compare
vonovak
added a commit
that referenced
this pull request
Oct 29, 2026
…207 changelog entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
force-pushed
the
vonovak/fix-reload-skipped-apps
branch
from
October 29, 2026 19:52
be2a099 to
fc40664
Compare
Rung 1 is decided on one observation made after the bundle gate: a fresh target list and the device facts that place it, read inside the run's remaining budget. Its scope, inventory and classified list serve the rest of the run: dispatch, evidence, counts and the relaunch. smoke reads the same budgeted observation for its reload. While the scope skips an app, rung 1 sends no broadcast. It sends CDP Page.reload to each own target that takes it, proves each on its target with the context marker, and keeps each target's outcome. All targets share one deadline. One replaced context proves the reload. When nothing is skipped, rung 1 stays the broadcast, also beside an app outside the scope, and the attempt says that it also reached that app. A fresh target, appsConnected and the relaunch app id count only the read group of the whole list. Peer churn beside an app outside the scope is "acted", not proved: the attempt is delivered, not ok, and the reload then needs a fresh own target or a bundle. A bundle proves only for a platform that no app outside the read group can fetch: never one a skipped app is on, and none after a broadcast that reached an app nothing places. runtime:reload and smoke end the reload in one function: it skips the wait when a new context already proved the reload, or when a Page.reload left no bundle that may count, and it names the strongest label whose evidence is in the payload. The failure text says which state the run ended in: nothing was watched, the churn was discounted, or the watched observations did not happen. Its How line names --eas and the tunnel only on a cloud run, and says no Page.reload was sent when no own app could keep the marker. The rules are in llp/0005 §How it reloads, §What proves a reload and §Target owner. The label rule and the evidence wait move to src/runtime/reload/evidence.ts, and the reloadAsync.ts cap goes down to 860. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reload cases beside another worktree's app become e2e cases in runtime-target-selection-test: Page.reload goes to this worktree's app only, a kept context is not counted, an app with no debugger gets no Page.reload, an app that appears during the bundle gate gets no broadcast, the device fallback stops only this worktree's app, and another platform's churn is not counted. The stub dev server gains a `pageReload` option and records Page.reload on a socket with no debugger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The other-worktree row had no device of its own: it moved the only binding to the other worktree, so the own app ran on a device nothing placed. It now binds its own simulator and puts another worktree's clone beside it, with the shared fixtures (bindOtherWorktreeAsync, runtimeTarget) instead of a hand-built registry file and a unit-test fixture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reload rules beside a skipped app cite the runtime-target-selection and smoke-target-selection e2e cases that replaced their unit tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The smoke reload rows and the bundle-gate re-read case read a booted local simulator, which exists only on macOS; on Linux and Windows they failed without testing the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the scope JSON and the skipped-app reason The bound simulator fixture carries a udid, an unused fixture import goes, and phases.ts holds the skipped-app reason together with this layer's lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hing can prove (llp/0005 §Target owner) A Page.reload and a local relaunch bring the app back under the page id it had. When the mechanism does that, the own app was listed before the run, and no bundle may count, the evidence wait is skipped. The rule read only whether the last attempt was a Page.reload, so a relaunch beside a skipped app waited out the whole --timeout. The result is unchanged: reloaded true, verifiedBy app-relaunch, exit 22. The failure text no longer says that the old runtime still answers after a relaunch. It says that a relaunched app registers again under the page id it had, and that nothing was watched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the first proof stops the other checks (llp/0005 §How it reloads) Rung 1's Page.reload proof has its own window, PAGE_RELOAD_PROOF_MS (8 s), beside the broadcast's churn window. A proof that does not come leaves the rest of --timeout to the relaunch in --method auto. smoke's reload uses the same rung and the same window. jsContextReplacedAsync takes a signal that aborts the phase it lands in. The first own target whose context is replaced stops the other checks. A stopped check still removes its marker, on a new session when the stop closed the old one. Its outcome is the new public value not-needed, in runtime:reload attempts[].outcomes and in smoke's reload.targetOutcomes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…207 changelog entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he exit code (llp/0005 §Target owner) installStubXcrunAsync binds this worktree to the first booted simulator, so the phone that appears during the bundle gate is another device and rung 1 must send Page.reload to the own app only. The case checked only that no broadcast was sent, which a run that exits before the socket write also passes. It now asserts pageReloads() and exit 0. With the binding off (AGENT_CLI_NO_DEVICE=1) the case fails: one broadcast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion stopped as not delivered, not unprovable (llp/0005 §What proves a reload) A mark that got no answer (the shared deadline already spent, an abort, a dropped connection) was `unmarkable`. When every own target ended that way, rung 1 said no app could keep the marker, which is the Expo Go Android case and not a spent budget. The mark now throws unless the app answered: a CDP error answer stays `unmarkable`, anything else is `not-delivered`, and a check stopped by another target's proof is `not-needed`. Unit test, not e2e: the stub dev server cannot hold the mark until the shared deadline passes without also holding the reads around it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e.reload that kept the context (llp/0005 §How it reloads) A delivered rung 1 (Page.reload answered, no new context) returned failure without looking. The policy still names a bundle platform when the skipped app is on another platform, as an Android smoke beside another worktree's iOS app is, and smoke never relaunches, so that bundle is the only proof left. smoke now calls observeReloadAsync for delivered too; `unwatched: 'nothing-can-prove'` stays the path where no bundle may count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…adcast when the read before rung 1 fails (llp/0005 §How it reloads) prepareRungOneAsync replaced the preflight placement with its own read, and a failed read looked like a list with nothing skipped. A probe that failed came back as an empty list, and a killed simctl left a cloud iOS scope with two undetermined apps; either way rung 1 broadcast to apps the run skips. Each call also got the whole remaining budget again. The probe and the device-tool reads (adb reverse included) now share one deadline. A failed probe, or a tool that did not answer where the preflight read had its answer, keeps the preflight observation, and rung 1 then sends no broadcast; Page.reload to the preflight's own targets still runs. smoke has no earlier read, so a failed probe there sends nothing. The e2e stub fails a /json/list read when a `targets` callback returns null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eps the CDP-error refusal beside the lost-answer rule Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unts the skipped apps instead of naming them again (llp/0005 §How it reloads) The run already names each skipped app on stderr and in skippedApps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as not delivered (llp/0005 §What proves a reload) Rung 1 maps the context proof's answer-lost kind to not-delivered, so it no longer reads the reason text to tell a lost answer from an answered one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… that nothing can prove (llp/0005 §Target owner) A cloud relaunch brings the session app back under its page id, as a local one does, so beside a skipped app of its platform no fresh target or bundle can arrive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ble, one discriminated shape The caller tests the kind instead of asking whether a session has a kind field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… it failed and no app is skipped, so it never says broadcast for a run that sent none (llp/0005 §How it reloads) rungOnePolicy takes the unread state, and runRungOneAsync switches on three mechanisms; the unread override on the prepared value is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tempt the run settled on, and a local relaunch whose wait saw nothing no longer calls the listed runtime the old one (llp/0005 §Target owner) observeReloadAsync derives the fact from the last attempt that reloaded or reached the app, through one table keyed by method; the keepsPageId option and its two caller expressions are gone, and the failure text forks read the same key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e tool stops answering on the read before rung 1 (llp/0005 §How it reloads) Smoke passes the last observation its own reads placed as rung 1's preflight, so the deviceFactsLost guard covers it as it covers runtime:reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e switch, so both decide "did rung 1 run" by the same rule (llp/0005 §How it reloads) settleRungOne maps each outcome to whether a mechanism ran, its own proof and the relaunch clause; the two copies of the not-delivered-or-unprovable test and the relaunchWhy field probe are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts stopped, so it no longer shares smoke's not-needed disposition (llp/0005 §How it reloads) The per-target outcome in attempts[].outcomes and smoke's reload.targetOutcomes is stopped; reload.disposition keeps not-needed for an app the run opened itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lives Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… same silent-tool rule as placement (llp/0005 §How it reloads) A tool that was already silent before rung 1 loses nothing, and a tool outside the scope's platform no longer counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he macOS job They skip off darwin, so no job ran them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y, where a local simulator can be relaunched Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
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 a reload reach only this worktree's apps when the run skips an app, and prove the reload with this worktree's apps only.
Stack: #202 → #203 → #204 → #206 → #217 → #218 → #207 → #200 (this is 7/8).
Problem
The dev server sends its reload broadcast to every app on its port. N2-17: a worktree bound to an iOS EAS session ran
runtime:reload. It printed "Skipped 1 app on local simulators … iPhone 17 Pro", then "Reloaded yes · via the dev server's client command socket … 2 client(s)". The broadcast also reloaded the skipped app.Fix
Rung 1 is decided on one target list read after the bundle gate. While the scope skips an app, rung 1 sends no broadcast. It sends CDP
Page.reloadto each own target and proves each with the context marker, under one shared deadline. When nothing is skipped, rung 1 stays the broadcast, and the attempt says when it also reached an app outside the scope. Peer churn beside an app outside the scope makes the attemptok: false, delivered: true: the reload then needs a fresh own target or a bundle. Nothing of an app outside the scope proves the reload. The failure text says whether nothing was watched, the churn was discounted, or the watched observations did not happen. When no own app could keep the marker, it says noPage.reloadwas sent. It names--easand the tunnel only on a cloud run.Page.reloadhas 8 s to prove itself, and the first proof stops the other checks (stopped). APage.reloadwhose answer was lost is not delivered. When the read before rung 1 fails, the run keeps its first placement and sends nothing (mechanismnone). After a relaunch that nothing can prove, local or cloud, the run exits 22 at once instead of waiting out--timeout. CI runs the reload cases that need a local simulator on the macOS job.Red tests: rungOne-test "a broadcast beside an app nothing places: no bundle proves it"; e2e runtime-target-selection-test "sends Page.reload to its own app only, and a page of the skipped app proves nothing ($pageReload)", "sends no Page.reload when its own app cannot keep the marker" and "runtime:reload does not take another platform's churn as its proof".
Live check (before the 8 s window and the relaunch exit; not rerun since): at the stack top, on an SDK 58 development build on a local iOS simulator. With a second app on another simulator, the run skipped it as
other-device, sentPage.reloadto its own app only, and proved 4 of 4 reloads bydebugger-context-replaced. With one app, it broadcast and proved 3 of 3 bymessage-socket-peers. On an EAS iOS session (Expo Go, v2 tunnel) with a local simulator app on the same dev server:runtime:reload --easskipped the local app asother-deviceand proved 2 of 2 reloads bydebugger-context-replaced;runtime:reload --iosskipped the cloud app and proved 3 of 3 the same way.Watch: when rung 1 broadcasts, an app that attaches after the last target list also gets the broadcast.
🤖 Generated with Claude Code